diff --git a/Pipfile.lock b/Pipfile.lock index 847519248..fa58bb7c3 100644 --- a/Pipfile.lock +++ b/Pipfile.lock @@ -16,13 +16,6 @@ ] }, "default": { - "aimmo": { - "hashes": [ - "sha256:58b90da42da179fbbeea141f6dbaff1cf5a81bfa06ec8a0edc1021da91bafda4", - "sha256:6e06d26335d76667c366e4bf7b8dc2edb3923af30fa59aaa70365cafe20efab2" - ], - "version": "==2.11.2" - }, "asgiref": { "hashes": [ "sha256:3e1e3ecc849832fe52ccf2cb6686b7a55f82bb1d6aee72a58826471390335e47", @@ -31,29 +24,13 @@ "markers": "python_version >= '3.8'", "version": "==3.8.1" }, - "attrs": { - "hashes": [ - "sha256:935dc3b529c262f6cf76e50877d35a4bd3c1de194fd41f47a2b7ae8f19971f30", - "sha256:99b87a485a5820b23b879f04c2305b44b951b502fd64be915879d77a7e8fc6f1" - ], - "markers": "python_version >= '3.7'", - "version": "==23.2.0" - }, - "cachetools": { - "hashes": [ - "sha256:0abad1021d3f8325b2fc1d2e9c8b9c9d57b04c3932657a72465447332c24d945", - "sha256:ba29e2dfa0b8b556606f097407ed1aa62080ee108ab0dc5ec9d6a723a007d105" - ], - "markers": "python_version >= '3.7'", - "version": "==5.3.3" - }, "certifi": { "hashes": [ - "sha256:0569859f95fc761b18b45ef421b1290a0f65f147e92a1e5eb3e635f9a5e4e66f", - "sha256:dc383c07b76109f368f6106eee2b593b04a011ea4d55f652c6ca24a754d1cdd1" + "sha256:5a1e7645bc0ec61a09e26c36f6106dd4cf40c6db3a1fb6352b0244e7fb057c7b", + "sha256:c198e21b1289c2ab85ee4e67bb4b4ef3ead0892059901a8d5b622f24a1101e90" ], "markers": "python_version >= '3.6'", - "version": "==2024.2.2" + "version": "==2024.7.4" }, "cfl-common": { "editable": true, @@ -159,14 +136,6 @@ "editable": true, "path": "." }, - "defusedxml": { - "hashes": [ - "sha256:1bb3032db185915b62d7c6209c5a8792be6a32ab2fedacc84e01b52c51aa3e69", - "sha256:a352e7e428770286cc899e2542b6cdaedb2b4953ff269a210103ec58f6198a61" - ], - "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4'", - "version": "==0.7.1" - }, "diff-match-patch": { "hashes": [ "sha256:953019cdb9c9d2c9e47b5b12bcff3cf4746fc4598eb406076fa1fc27e6a1f15c", @@ -213,11 +182,11 @@ }, "django-import-export": { "hashes": [ - "sha256:2eac09e8cec8670f36e24314760448011ad23c51e8fb930d55f50d0c3c926da0", - "sha256:4deabc557801d368093608c86fd0f4831bc9540e2ea41ca2f023e2efb3eb6f48" + "sha256:16ecc5a9f0df46bde6eb278a3e65ebda0ee1db55656f36440e9fb83f40ab85a3", + "sha256:730ae2443a02b1ba27d8dba078a27ae9123adfcabb78161b4f130843607b3df9" ], "markers": "python_version >= '3.8'", - "version": "==3.3.8" + "version": "==4.1.1" }, "django-js-reverse": { "hashes": [ @@ -296,109 +265,6 @@ "markers": "python_version >= '3.6'", "version": "==3.13.1" }, - "dnspython": { - "hashes": [ - "sha256:36c5e8e38d4369a08b6780b7f27d790a292b2b08eea01607865bf0936c558e01", - "sha256:f69c21288a962f4da86e56c4905b49d11aba7938d3d740e80d9e366ee4f1632d" - ], - "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'", - "version": "==1.16.0" - }, - "et-xmlfile": { - "hashes": [ - "sha256:8eb9e2bc2f8c97e37a2dc85a09ecdcdec9d8a396530a6d5a33b30b9a92da0c5c", - "sha256:a2ba85d1d6a74ef63837eed693bcb89c3f752169b0e3e7ae5b16ca5e1b3deada" - ], - "markers": "python_version >= '3.6'", - "version": "==1.1.0" - }, - "eventlet": { - "hashes": [ - "sha256:27ae41fad9deed9bbf4166f3e3b65acc15d524d42210a518e5877da85a6b8c5d", - "sha256:b36ec2ecc003de87fc87b93197d77fea528aa0f9204a34fdf3b2f8d0f01e017b" - ], - "version": "==0.31.0" - }, - "google-auth": { - "hashes": [ - "sha256:672dff332d073227550ffc7457868ac4218d6c500b155fe6cc17d2b13602c360", - "sha256:d452ad095688cd52bae0ad6fafe027f6a6d6f560e810fec20914e17a09526415" - ], - "markers": "python_version >= '3.7'", - "version": "==2.29.0" - }, - "greenlet": { - "hashes": [ - "sha256:01bc7ea167cf943b4c802068e178bbf70ae2e8c080467070d01bfa02f337ee67", - "sha256:0448abc479fab28b00cb472d278828b3ccca164531daab4e970a0458786055d6", - "sha256:086152f8fbc5955df88382e8a75984e2bb1c892ad2e3c80a2508954e52295257", - "sha256:098d86f528c855ead3479afe84b49242e174ed262456c342d70fc7f972bc13c4", - "sha256:149e94a2dd82d19838fe4b2259f1b6b9957d5ba1b25640d2380bea9c5df37676", - "sha256:1551a8195c0d4a68fac7a4325efac0d541b48def35feb49d803674ac32582f61", - "sha256:15d79dd26056573940fcb8c7413d84118086f2ec1a8acdfa854631084393efcc", - "sha256:1996cb9306c8595335bb157d133daf5cf9f693ef413e7673cb07e3e5871379ca", - "sha256:1a7191e42732df52cb5f39d3527217e7ab73cae2cb3694d241e18f53d84ea9a7", - "sha256:1ea188d4f49089fc6fb283845ab18a2518d279c7cd9da1065d7a84e991748728", - "sha256:1f672519db1796ca0d8753f9e78ec02355e862d0998193038c7073045899f305", - "sha256:2516a9957eed41dd8f1ec0c604f1cdc86758b587d964668b5b196a9db5bfcde6", - "sha256:2797aa5aedac23af156bbb5a6aa2cd3427ada2972c828244eb7d1b9255846379", - "sha256:2dd6e660effd852586b6a8478a1d244b8dc90ab5b1321751d2ea15deb49ed414", - "sha256:3ddc0f794e6ad661e321caa8d2f0a55ce01213c74722587256fb6566049a8b04", - "sha256:3ed7fb269f15dc662787f4119ec300ad0702fa1b19d2135a37c2c4de6fadfd4a", - "sha256:419b386f84949bf0e7c73e6032e3457b82a787c1ab4a0e43732898a761cc9dbf", - "sha256:43374442353259554ce33599da8b692d5aa96f8976d567d4badf263371fbe491", - "sha256:52f59dd9c96ad2fc0d5724107444f76eb20aaccb675bf825df6435acb7703559", - "sha256:57e8974f23e47dac22b83436bdcf23080ade568ce77df33159e019d161ce1d1e", - "sha256:5b51e85cb5ceda94e79d019ed36b35386e8c37d22f07d6a751cb659b180d5274", - "sha256:649dde7de1a5eceb258f9cb00bdf50e978c9db1b996964cd80703614c86495eb", - "sha256:64d7675ad83578e3fc149b617a444fab8efdafc9385471f868eb5ff83e446b8b", - "sha256:68834da854554926fbedd38c76e60c4a2e3198c6fbed520b106a8986445caaf9", - "sha256:6b66c9c1e7ccabad3a7d037b2bcb740122a7b17a53734b7d72a344ce39882a1b", - "sha256:70fb482fdf2c707765ab5f0b6655e9cfcf3780d8d87355a063547b41177599be", - "sha256:7170375bcc99f1a2fbd9c306f5be8764eaf3ac6b5cb968862cad4c7057756506", - "sha256:73a411ef564e0e097dbe7e866bb2dda0f027e072b04da387282b02c308807405", - "sha256:77457465d89b8263bca14759d7c1684df840b6811b2499838cc5b040a8b5b113", - "sha256:7f362975f2d179f9e26928c5b517524e89dd48530a0202570d55ad6ca5d8a56f", - "sha256:81bb9c6d52e8321f09c3d165b2a78c680506d9af285bfccbad9fb7ad5a5da3e5", - "sha256:881b7db1ebff4ba09aaaeae6aa491daeb226c8150fc20e836ad00041bcb11230", - "sha256:894393ce10ceac937e56ec00bb71c4c2f8209ad516e96033e4b3b1de270e200d", - "sha256:99bf650dc5d69546e076f413a87481ee1d2d09aaaaaca058c9251b6d8c14783f", - "sha256:9da2bd29ed9e4f15955dd1595ad7bc9320308a3b766ef7f837e23ad4b4aac31a", - "sha256:afaff6cf5200befd5cec055b07d1c0a5a06c040fe5ad148abcd11ba6ab9b114e", - "sha256:b1b5667cced97081bf57b8fa1d6bfca67814b0afd38208d52538316e9422fc61", - "sha256:b37eef18ea55f2ffd8f00ff8fe7c8d3818abd3e25fb73fae2ca3b672e333a7a6", - "sha256:b542be2440edc2d48547b5923c408cbe0fc94afb9f18741faa6ae970dbcb9b6d", - "sha256:b7dcbe92cc99f08c8dd11f930de4d99ef756c3591a5377d1d9cd7dd5e896da71", - "sha256:b7f009caad047246ed379e1c4dbcb8b020f0a390667ea74d2387be2998f58a22", - "sha256:bba5387a6975598857d86de9eac14210a49d554a77eb8261cc68b7d082f78ce2", - "sha256:c5e1536de2aad7bf62e27baf79225d0d64360d4168cf2e6becb91baf1ed074f3", - "sha256:c5ee858cfe08f34712f548c3c363e807e7186f03ad7a5039ebadb29e8c6be067", - "sha256:c9db1c18f0eaad2f804728c67d6c610778456e3e1cc4ab4bbd5eeb8e6053c6fc", - "sha256:d353cadd6083fdb056bb46ed07e4340b0869c305c8ca54ef9da3421acbdf6881", - "sha256:d46677c85c5ba00a9cb6f7a00b2bfa6f812192d2c9f7d9c4f6a55b60216712f3", - "sha256:d4d1ac74f5c0c0524e4a24335350edad7e5f03b9532da7ea4d3c54d527784f2e", - "sha256:d73a9fe764d77f87f8ec26a0c85144d6a951a6c438dfe50487df5595c6373eac", - "sha256:da70d4d51c8b306bb7a031d5cff6cc25ad253affe89b70352af5f1cb68e74b53", - "sha256:daf3cb43b7cf2ba96d614252ce1684c1bccee6b2183a01328c98d36fcd7d5cb0", - "sha256:dca1e2f3ca00b84a396bc1bce13dd21f680f035314d2379c4160c98153b2059b", - "sha256:dd4f49ae60e10adbc94b45c0b5e6a179acc1736cf7a90160b404076ee283cf83", - "sha256:e1f145462f1fa6e4a4ae3c0f782e580ce44d57c8f2c7aae1b6fa88c0b2efdb41", - "sha256:e3391d1e16e2a5a1507d83e4a8b100f4ee626e8eca43cf2cadb543de69827c4c", - "sha256:fcd2469d6a2cf298f198f0487e0a5b1a47a42ca0fa4dfd1b6862c999f018ebbf", - "sha256:fd096eb7ffef17c456cfa587523c5f92321ae02427ff955bebe9e3c63bc9f0da", - "sha256:fe754d231288e1e64323cfad462fcee8f0288654c10bdf4f603a39ed923bef33" - ], - "markers": "python_version >= '3.7'", - "version": "==3.0.3" - }, - "hypothesis": { - "hashes": [ - "sha256:6a3471ff74864ab04a0650c75500ef15f2f4a901d49ccbb7cbec668365736688", - "sha256:989162a9e0715c624b99ad9b2b4206765879b40eb51eef17b1e37de3e898370a" - ], - "markers": "python_version >= '3.6'", - "version": "==5.41.3" - }, "idna": { "hashes": [ "sha256:028ff3aadf0609c1fd278d8ea3089299412a7a8b9bd005dd08b9f8285bcb5cfc", @@ -415,14 +281,6 @@ "markers": "python_version >= '3.7'", "version": "==4.13.0" }, - "kubernetes": { - "hashes": [ - "sha256:5854b0c508e8d217ca205591384ab58389abdae608576f9c9afc35a3c76a366c", - "sha256:e3db6800abf7e36c38d2629b5cb6b74d10988ee0cba6fba45595a7cbe60c0042" - ], - "markers": "python_version >= '3.6'", - "version": "==26.1.0" - }, "libsass": { "hashes": [ "sha256:31e86d92a5c7a551df844b72d83fc2b5e50abc6fbbb31e296f7bebd6489ed1b4", @@ -435,12 +293,6 @@ "markers": "python_version >= '3.8'", "version": "==0.23.0" }, - "markuppy": { - "hashes": [ - "sha256:1adee2c0a542af378fe84548ff6f6b0168f3cb7f426b46961038a2bcfaad0d5f" - ], - "version": "==1.14" - }, "more-itertools": { "hashes": [ "sha256:5652a9ac72209ed7df8d9c15daf4e1aa0e3d2ccd3c87f8265a0673cd9cbc9ced", @@ -483,28 +335,6 @@ "markers": "python_version >= '3.8'", "version": "==1.24.4" }, - "oauthlib": { - "hashes": [ - "sha256:8139f29aac13e25d502680e9e19963e83f16838d48a0d71c287fe40e7067fbca", - "sha256:9859c40929662bec5d64f34d01c99e093149682a3f38915dc0655d5a633dd918" - ], - "markers": "python_version >= '3.6'", - "version": "==3.2.2" - }, - "odfpy": { - "hashes": [ - "sha256:db766a6e59c5103212f3cc92ec8dd50a0f3a02790233ed0b52148b70d3c438ec", - "sha256:fc3b8d1bc098eba4a0fda865a76d9d1e577c4ceec771426bcb169a82c5e9dfe0" - ], - "version": "==1.4.1" - }, - "openpyxl": { - "hashes": [ - "sha256:a6f5977418eff3b2d5500d54d9db50c8277a368436f4e4f8ddb1be3422870184", - "sha256:f91456ead12ab3c6c2e9491cf33ba6d08357d802192379bb482f1033ade496f5" - ], - "version": "==3.1.2" - }, "pandas": { "hashes": [ "sha256:04dbdbaf2e4d46ca8da896e1805bc04eb85caa9a82e259e8eed00254d5e0c682", @@ -553,94 +383,89 @@ }, "pillow": { "hashes": [ - "sha256:048ad577748b9fa4a99a0548c64f2cb8d672d5bf2e643a739ac8faff1164238c", - "sha256:048eeade4c33fdf7e08da40ef402e748df113fd0b4584e32c4af74fe78baaeb2", - "sha256:0ba26351b137ca4e0db0342d5d00d2e355eb29372c05afd544ebf47c0956ffeb", - "sha256:0ea2a783a2bdf2a561808fe4a7a12e9aa3799b701ba305de596bc48b8bdfce9d", - "sha256:1530e8f3a4b965eb6a7785cf17a426c779333eb62c9a7d1bbcf3ffd5bf77a4aa", - "sha256:16563993329b79513f59142a6b02055e10514c1a8e86dca8b48a893e33cf91e3", - "sha256:19aeb96d43902f0a783946a0a87dbdad5c84c936025b8419da0a0cd7724356b1", - "sha256:1a1d1915db1a4fdb2754b9de292642a39a7fb28f1736699527bb649484fb966a", - "sha256:1b87bd9d81d179bd8ab871603bd80d8645729939f90b71e62914e816a76fc6bd", - "sha256:1dfc94946bc60ea375cc39cff0b8da6c7e5f8fcdc1d946beb8da5c216156ddd8", - "sha256:2034f6759a722da3a3dbd91a81148cf884e91d1b747992ca288ab88c1de15999", - "sha256:261ddb7ca91fcf71757979534fb4c128448b5b4c55cb6152d280312062f69599", - "sha256:2ed854e716a89b1afcedea551cd85f2eb2a807613752ab997b9974aaa0d56936", - "sha256:3102045a10945173d38336f6e71a8dc71bcaeed55c3123ad4af82c52807b9375", - "sha256:339894035d0ede518b16073bdc2feef4c991ee991a29774b33e515f1d308e08d", - "sha256:412444afb8c4c7a6cc11a47dade32982439925537e483be7c0ae0cf96c4f6a0b", - "sha256:4203efca580f0dd6f882ca211f923168548f7ba334c189e9eab1178ab840bf60", - "sha256:45ebc7b45406febf07fef35d856f0293a92e7417ae7933207e90bf9090b70572", - "sha256:4b5ec25d8b17217d635f8935dbc1b9aa5907962fae29dff220f2659487891cd3", - "sha256:4c8e73e99da7db1b4cad7f8d682cf6abad7844da39834c288fbfa394a47bbced", - "sha256:4e6f7d1c414191c1199f8996d3f2282b9ebea0945693fb67392c75a3a320941f", - "sha256:4eaa22f0d22b1a7e93ff0a596d57fdede2e550aecffb5a1ef1106aaece48e96b", - "sha256:50b8eae8f7334ec826d6eeffaeeb00e36b5e24aa0b9df322c247539714c6df19", - "sha256:50fd3f6b26e3441ae07b7c979309638b72abc1a25da31a81a7fbd9495713ef4f", - "sha256:51243f1ed5161b9945011a7360e997729776f6e5d7005ba0c6879267d4c5139d", - "sha256:5d512aafa1d32efa014fa041d38868fda85028e3f930a96f85d49c7d8ddc0383", - "sha256:5f77cf66e96ae734717d341c145c5949c63180842a545c47a0ce7ae52ca83795", - "sha256:6b02471b72526ab8a18c39cb7967b72d194ec53c1fd0a70b050565a0f366d355", - "sha256:6fb1b30043271ec92dc65f6d9f0b7a830c210b8a96423074b15c7bc999975f57", - "sha256:7161ec49ef0800947dc5570f86568a7bb36fa97dd09e9827dc02b718c5643f09", - "sha256:72d622d262e463dfb7595202d229f5f3ab4b852289a1cd09650362db23b9eb0b", - "sha256:74d28c17412d9caa1066f7a31df8403ec23d5268ba46cd0ad2c50fb82ae40462", - "sha256:78618cdbccaa74d3f88d0ad6cb8ac3007f1a6fa5c6f19af64b55ca170bfa1edf", - "sha256:793b4e24db2e8742ca6423d3fde8396db336698c55cd34b660663ee9e45ed37f", - "sha256:798232c92e7665fe82ac085f9d8e8ca98826f8e27859d9a96b41d519ecd2e49a", - "sha256:81d09caa7b27ef4e61cb7d8fbf1714f5aec1c6b6c5270ee53504981e6e9121ad", - "sha256:8ab74c06ffdab957d7670c2a5a6e1a70181cd10b727cd788c4dd9005b6a8acd9", - "sha256:8eb0908e954d093b02a543dc963984d6e99ad2b5e36503d8a0aaf040505f747d", - "sha256:90b9e29824800e90c84e4022dd5cc16eb2d9605ee13f05d47641eb183cd73d45", - "sha256:9797a6c8fe16f25749b371c02e2ade0efb51155e767a971c61734b1bf6293994", - "sha256:9d2455fbf44c914840c793e89aa82d0e1763a14253a000743719ae5946814b2d", - "sha256:9d3bea1c75f8c53ee4d505c3e67d8c158ad4df0d83170605b50b64025917f338", - "sha256:9e2ec1e921fd07c7cda7962bad283acc2f2a9ccc1b971ee4b216b75fad6f0463", - "sha256:9e91179a242bbc99be65e139e30690e081fe6cb91a8e77faf4c409653de39451", - "sha256:a0eaa93d054751ee9964afa21c06247779b90440ca41d184aeb5d410f20ff591", - "sha256:a2c405445c79c3f5a124573a051062300936b0281fee57637e706453e452746c", - "sha256:aa7e402ce11f0885305bfb6afb3434b3cd8f53b563ac065452d9d5654c7b86fd", - "sha256:aff76a55a8aa8364d25400a210a65ff59d0168e0b4285ba6bf2bd83cf675ba32", - "sha256:b09b86b27a064c9624d0a6c54da01c1beaf5b6cadfa609cf63789b1d08a797b9", - "sha256:b14f16f94cbc61215115b9b1236f9c18403c15dd3c52cf629072afa9d54c1cbf", - "sha256:b50811d664d392f02f7761621303eba9d1b056fb1868c8cdf4231279645c25f5", - "sha256:b7bc2176354defba3edc2b9a777744462da2f8e921fbaf61e52acb95bafa9828", - "sha256:c78e1b00a87ce43bb37642c0812315b411e856a905d58d597750eb79802aaaa3", - "sha256:c83341b89884e2b2e55886e8fbbf37c3fa5efd6c8907124aeb72f285ae5696e5", - "sha256:ca2870d5d10d8726a27396d3ca4cf7976cec0f3cb706debe88e3a5bd4610f7d2", - "sha256:ccce24b7ad89adb5a1e34a6ba96ac2530046763912806ad4c247356a8f33a67b", - "sha256:cd5e14fbf22a87321b24c88669aad3a51ec052eb145315b3da3b7e3cc105b9a2", - "sha256:ce49c67f4ea0609933d01c0731b34b8695a7a748d6c8d186f95e7d085d2fe475", - "sha256:d33891be6df59d93df4d846640f0e46f1a807339f09e79a8040bc887bdcd7ed3", - "sha256:d3b2348a78bc939b4fed6552abfd2e7988e0f81443ef3911a4b8498ca084f6eb", - "sha256:d886f5d353333b4771d21267c7ecc75b710f1a73d72d03ca06df49b09015a9ef", - "sha256:d93480005693d247f8346bc8ee28c72a2191bdf1f6b5db469c096c0c867ac015", - "sha256:dc1a390a82755a8c26c9964d457d4c9cbec5405896cba94cf51f36ea0d855002", - "sha256:dd78700f5788ae180b5ee8902c6aea5a5726bac7c364b202b4b3e3ba2d293170", - "sha256:e46f38133e5a060d46bd630faa4d9fa0202377495df1f068a8299fd78c84de84", - "sha256:e4b878386c4bf293578b48fc570b84ecfe477d3b77ba39a6e87150af77f40c57", - "sha256:f0d0591a0aeaefdaf9a5e545e7485f89910c977087e7de2b6c388aec32011e9f", - "sha256:fdcbb4068117dfd9ce0138d068ac512843c52295ed996ae6dd1faf537b6dbc27", - "sha256:ff61bfd9253c3915e6d41c651d5f962da23eda633cf02262990094a18a55371a" - ], - "markers": "python_version >= '3.8'", - "version": "==10.3.0" - }, - "pyasn1": { - "hashes": [ - "sha256:3a35ab2c4b5ef98e17dfdec8ab074046fbda76e281c5a706ccd82328cfc8f64c", - "sha256:cca4bb0f2df5504f02f6f8a775b6e416ff9b0b3b16f7ee80b5a3153d9b804473" - ], - "markers": "python_version >= '3.8'", - "version": "==0.6.0" - }, - "pyasn1-modules": { - "hashes": [ - "sha256:831dbcea1b177b28c9baddf4c6d1013c24c3accd14a1873fffaa6a2e905f17b6", - "sha256:be04f15b66c206eed667e0bb5ab27e2b1855ea54a842e5037738099e8ca4ae0b" + "sha256:02a2be69f9c9b8c1e97cf2713e789d4e398c751ecfd9967c18d0ce304efbf885", + "sha256:030abdbe43ee02e0de642aee345efa443740aa4d828bfe8e2eb11922ea6a21ea", + "sha256:06b2f7898047ae93fad74467ec3d28fe84f7831370e3c258afa533f81ef7f3df", + "sha256:0755ffd4a0c6f267cccbae2e9903d95477ca2f77c4fcf3a3a09570001856c8a5", + "sha256:0a9ec697746f268507404647e531e92889890a087e03681a3606d9b920fbee3c", + "sha256:0ae24a547e8b711ccaaf99c9ae3cd975470e1a30caa80a6aaee9a2f19c05701d", + "sha256:134ace6dc392116566980ee7436477d844520a26a4b1bd4053f6f47d096997fd", + "sha256:166c1cd4d24309b30d61f79f4a9114b7b2313d7450912277855ff5dfd7cd4a06", + "sha256:1b5dea9831a90e9d0721ec417a80d4cbd7022093ac38a568db2dd78363b00908", + "sha256:1d846aea995ad352d4bdcc847535bd56e0fd88d36829d2c90be880ef1ee4668a", + "sha256:1ef61f5dd14c300786318482456481463b9d6b91ebe5ef12f405afbba77ed0be", + "sha256:297e388da6e248c98bc4a02e018966af0c5f92dfacf5a5ca22fa01cb3179bca0", + "sha256:298478fe4f77a4408895605f3482b6cc6222c018b2ce565c2b6b9c354ac3229b", + "sha256:29dbdc4207642ea6aad70fbde1a9338753d33fb23ed6956e706936706f52dd80", + "sha256:2db98790afc70118bd0255c2eeb465e9767ecf1f3c25f9a1abb8ffc8cfd1fe0a", + "sha256:32cda9e3d601a52baccb2856b8ea1fc213c90b340c542dcef77140dfa3278a9e", + "sha256:37fb69d905be665f68f28a8bba3c6d3223c8efe1edf14cc4cfa06c241f8c81d9", + "sha256:416d3a5d0e8cfe4f27f574362435bc9bae57f679a7158e0096ad2beb427b8696", + "sha256:43efea75eb06b95d1631cb784aa40156177bf9dd5b4b03ff38979e048258bc6b", + "sha256:4b35b21b819ac1dbd1233317adeecd63495f6babf21b7b2512d244ff6c6ce309", + "sha256:4d9667937cfa347525b319ae34375c37b9ee6b525440f3ef48542fcf66f2731e", + "sha256:5161eef006d335e46895297f642341111945e2c1c899eb406882a6c61a4357ab", + "sha256:543f3dc61c18dafb755773efc89aae60d06b6596a63914107f75459cf984164d", + "sha256:551d3fd6e9dc15e4c1eb6fc4ba2b39c0c7933fa113b220057a34f4bb3268a060", + "sha256:59291fb29317122398786c2d44427bbd1a6d7ff54017075b22be9d21aa59bd8d", + "sha256:5b001114dd152cfd6b23befeb28d7aee43553e2402c9f159807bf55f33af8a8d", + "sha256:5b4815f2e65b30f5fbae9dfffa8636d992d49705723fe86a3661806e069352d4", + "sha256:5dc6761a6efc781e6a1544206f22c80c3af4c8cf461206d46a1e6006e4429ff3", + "sha256:5e84b6cc6a4a3d76c153a6b19270b3526a5a8ed6b09501d3af891daa2a9de7d6", + "sha256:6209bb41dc692ddfee4942517c19ee81b86c864b626dbfca272ec0f7cff5d9fb", + "sha256:673655af3eadf4df6b5457033f086e90299fdd7a47983a13827acf7459c15d94", + "sha256:6c762a5b0997f5659a5ef2266abc1d8851ad7749ad9a6a5506eb23d314e4f46b", + "sha256:7086cc1d5eebb91ad24ded9f58bec6c688e9f0ed7eb3dbbf1e4800280a896496", + "sha256:73664fe514b34c8f02452ffb73b7a92c6774e39a647087f83d67f010eb9a0cf0", + "sha256:76a911dfe51a36041f2e756b00f96ed84677cdeb75d25c767f296c1c1eda1319", + "sha256:780c072c2e11c9b2c7ca37f9a2ee8ba66f44367ac3e5c7832afcfe5104fd6d1b", + "sha256:7928ecbf1ece13956b95d9cbcfc77137652b02763ba384d9ab508099a2eca856", + "sha256:7970285ab628a3779aecc35823296a7869f889b8329c16ad5a71e4901a3dc4ef", + "sha256:7a8d4bade9952ea9a77d0c3e49cbd8b2890a399422258a77f357b9cc9be8d680", + "sha256:7c1ee6f42250df403c5f103cbd2768a28fe1a0ea1f0f03fe151c8741e1469c8b", + "sha256:7dfecdbad5c301d7b5bde160150b4db4c659cee2b69589705b6f8a0c509d9f42", + "sha256:812f7342b0eee081eaec84d91423d1b4650bb9828eb53d8511bcef8ce5aecf1e", + "sha256:866b6942a92f56300012f5fbac71f2d610312ee65e22f1aa2609e491284e5597", + "sha256:86dcb5a1eb778d8b25659d5e4341269e8590ad6b4e8b44d9f4b07f8d136c414a", + "sha256:87dd88ded2e6d74d31e1e0a99a726a6765cda32d00ba72dc37f0651f306daaa8", + "sha256:8bc1a764ed8c957a2e9cacf97c8b2b053b70307cf2996aafd70e91a082e70df3", + "sha256:8d4d5063501b6dd4024b8ac2f04962d661222d120381272deea52e3fc52d3736", + "sha256:8f0aef4ef59694b12cadee839e2ba6afeab89c0f39a3adc02ed51d109117b8da", + "sha256:930044bb7679ab003b14023138b50181899da3f25de50e9dbee23b61b4de2126", + "sha256:950be4d8ba92aca4b2bb0741285a46bfae3ca699ef913ec8416c1b78eadd64cd", + "sha256:961a7293b2457b405967af9c77dcaa43cc1a8cd50d23c532e62d48ab6cdd56f5", + "sha256:9b885f89040bb8c4a1573566bbb2f44f5c505ef6e74cec7ab9068c900047f04b", + "sha256:9f4727572e2918acaa9077c919cbbeb73bd2b3ebcfe033b72f858fc9fbef0026", + "sha256:a02364621fe369e06200d4a16558e056fe2805d3468350df3aef21e00d26214b", + "sha256:a985e028fc183bf12a77a8bbf36318db4238a3ded7fa9df1b9a133f1cb79f8fc", + "sha256:ac1452d2fbe4978c2eec89fb5a23b8387aba707ac72810d9490118817d9c0b46", + "sha256:b15e02e9bb4c21e39876698abf233c8c579127986f8207200bc8a8f6bb27acf2", + "sha256:b2724fdb354a868ddf9a880cb84d102da914e99119211ef7ecbdc613b8c96b3c", + "sha256:bbc527b519bd3aa9d7f429d152fea69f9ad37c95f0b02aebddff592688998abe", + "sha256:bcd5e41a859bf2e84fdc42f4edb7d9aba0a13d29a2abadccafad99de3feff984", + "sha256:bd2880a07482090a3bcb01f4265f1936a903d70bc740bfcb1fd4e8a2ffe5cf5a", + "sha256:bee197b30783295d2eb680b311af15a20a8b24024a19c3a26431ff83eb8d1f70", + "sha256:bf2342ac639c4cf38799a44950bbc2dfcb685f052b9e262f446482afaf4bffca", + "sha256:c76e5786951e72ed3686e122d14c5d7012f16c8303a674d18cdcd6d89557fc5b", + "sha256:cbed61494057c0f83b83eb3a310f0bf774b09513307c434d4366ed64f4128a91", + "sha256:cfdd747216947628af7b259d274771d84db2268ca062dd5faf373639d00113a3", + "sha256:d7480af14364494365e89d6fddc510a13e5a2c3584cb19ef65415ca57252fb84", + "sha256:dbc6ae66518ab3c5847659e9988c3b60dc94ffb48ef9168656e0019a93dbf8a1", + "sha256:dc3e2db6ba09ffd7d02ae9141cfa0ae23393ee7687248d46a7507b75d610f4f5", + "sha256:dfe91cb65544a1321e631e696759491ae04a2ea11d36715eca01ce07284738be", + "sha256:e4d49b85c4348ea0b31ea63bc75a9f3857869174e2bf17e7aba02945cd218e6f", + "sha256:e4db64794ccdf6cb83a59d73405f63adbe2a1887012e308828596100a0b2f6cc", + "sha256:e553cad5179a66ba15bb18b353a19020e73a7921296a7979c4a2b7f6a5cd57f9", + "sha256:e88d5e6ad0d026fba7bdab8c3f225a69f063f116462c49892b0149e21b6c0a0e", + "sha256:ecd85a8d3e79cd7158dec1c9e5808e821feea088e2f69a974db5edf84dc53141", + "sha256:f5b92f4d70791b4a67157321c4e8225d60b119c5cc9aee8ecf153aace4aad4ef", + "sha256:f5f0c3e969c8f12dd2bb7e0b15d5c468b51e5017e01e2e867335c81903046a22", + "sha256:f7baece4ce06bade126fb84b8af1c33439a76d8a6fd818970215e0560ca28c27", + "sha256:ff25afb18123cea58a591ea0244b92eb1e61a1fd497bf6d6384f09bc3262ec3e", + "sha256:ff337c552345e95702c5fde3158acb0625111017d0e5f24bf3acdb9cc16b90d1" ], "markers": "python_version >= '3.8'", - "version": "==0.4.0" + "version": "==10.4.0" }, "pyhamcrest": { "hashes": [ @@ -670,7 +495,7 @@ "sha256:37dd54208da7e1cd875388217d5e00ebd4179249f90fb72437e91a35459a0ad3", "sha256:a8b2bc7bffae282281c8140a97d3aa9c14da0b136dfe83f850eea9a5f7470427" ], - "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'", + "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2'", "version": "==2.9.0.post0" }, "pytz": { @@ -725,10 +550,10 @@ }, "rapid-router": { "hashes": [ - "sha256:5c1ddef2abc6dce795b48baa9eb73ba46bfc21f6d99e635ded23633bc6a2ec11", - "sha256:64f8a71b4bf2fe8b47b248935f04ac242e012954bcf243dcd2321c76e988b479" + "sha256:b5e28378a88a7ea73090d18ce9cfd9de7d57163ea0802984aa90ed9c2e65af80", + "sha256:d3163dc3cb3bebe137e9eba428fdb675f2a487f3b8c2a17e80f6b014f4bbad7c" ], - "version": "==5.16.22" + "version": "==6.0.0" }, "reportlab": { "hashes": [ @@ -783,67 +608,37 @@ }, "requests": { "hashes": [ - "sha256:58cd2187c01e70e6e26505bca751777aa9f2ee0b7f4300988b709f44e013003f", - "sha256:942c5a758f98d790eaed1a29cb6eefc7ffb0d1cf7af05c3d2791656dbd6ad1e1" + "sha256:dd951ff5ecf3e3b3aa26b40703ba77495dab41da839ae72ef3c8e5d8e2433289", + "sha256:fc06670dd0ed212426dfeb94fc1b983d917c4f9847c863f313c9dfaaffb7c23c" ], - "markers": "python_version >= '3.7'", - "version": "==2.31.0" - }, - "requests-oauthlib": { - "hashes": [ - "sha256:7dd8a5c40426b779b0868c404bdef9768deccf22749cde15852df527e6269b36", - "sha256:b3dffaebd884d8cd778494369603a9e7b58d29111bf6b41bdc2dcd87203af4e9" - ], - "markers": "python_version >= '3.4'", - "version": "==2.0.0" - }, - "rsa": { - "hashes": [ - "sha256:90260d9058e514786967344d0ef75fa8727eed8a7d2e43ce9f4bcf1b536174f7", - "sha256:e38464a49c6c85d7f1351b0126661487a7e0a14a50f1675ec50eb34d4f20ef21" - ], - "markers": "python_version >= '3.6' and python_version < '4'", - "version": "==4.9" + "markers": "python_version >= '3.8'", + "version": "==2.32.2" }, "setuptools": { "hashes": [ - "sha256:d0b9a8433464d5800cbe05094acf5c6d52a91bfac9b52bcfc4d41382be5d5d31", - "sha256:e197a19aa8ec9722928f2206f8de752def0e4c9fc6953527360d1c36d94ddb2f" + "sha256:f171bab1dfbc86b132997f26a119f6056a57950d058587841a0082e8830f9dc5", + "sha256:fe384da74336c398e0d956d1cae0669bc02eed936cdb1d49b57de1990dc11ffc" ], - "markers": "python_version >= '3.7'", - "version": "==65.5.1" + "markers": "python_version >= '3.8'", + "version": "==70.3.0" }, "six": { "hashes": [ "sha256:1e61c37477a1626458e36f7b1d82aa5c9b094fa4802892072e49de9c60c4c926", "sha256:8abb2f1d86890a2dfb989f9a77cfcfd3e47c2a354b01111771326f8aa26e0254" ], - "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'", + "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2'", "version": "==1.16.0" }, - "sortedcontainers": { - "hashes": [ - "sha256:25caa5a06cc30b6b83d11423433f65d1f9d76c4c6a0c90e3379eaa43b9bfdb88", - "sha256:a163dcaede0f1c021485e957a39245190e74249897e2ae4b2aa38595db237ee0" - ], - "version": "==2.4.0" - }, "sqlparse": { "hashes": [ - "sha256:714d0a4932c059d16189f58ef5411ec2287a4360f17cdd0edd2d09d4c5087c93", - "sha256:c204494cd97479d0e39f28c93d46c0b2d5959c7b9ab904762ea6c7af211c8663" + "sha256:773dcbf9a5ab44a090f3441e2180efe2560220203dc2f8c0b0fa141e18b505e4", + "sha256:bb6b4df465655ef332548e24f08e205afc81b9ab86cb1c45657a7ff173a3a00e" ], "markers": "python_version >= '3.8'", - "version": "==0.5.0" + "version": "==0.5.1" }, "tablib": { - "extras": [ - "html", - "ods", - "xls", - "xlsx", - "yaml" - ], "hashes": [ "sha256:9821caa9eca6062ff7299fa645e737aecff982e6b2b42046928a6413c8dabfd9", "sha256:f6661dfc45e1d4f51fa8a6239f9c8349380859a5bfaa73280645f046d6c96e33" @@ -853,11 +648,11 @@ }, "typing-extensions": { "hashes": [ - "sha256:83f085bd5ca59c80295fc2a82ab5dac679cbe02b9f33f7d83af68e241bea51b0", - "sha256:c1f94d72897edaf4ce775bb7558d5b79d8126906a14ea5ed1635921406c0387a" + "sha256:04e5ca0351e0f3f85c6853954072df659d0d13fac324d0072316b67d7794700d", + "sha256:1a7ead55c7e559dd4dee8856e3a88b41225abfe1ce8df57b7c13915fe121ffb8" ], "markers": "python_version >= '3.8'", - "version": "==4.11.0" + "version": "==4.12.2" }, "tzdata": { "hashes": [ @@ -869,41 +664,19 @@ }, "urllib3": { "hashes": [ - "sha256:450b20ec296a467077128bff42b73080516e71b56ff59a60a02bef2232c4fa9d", - "sha256:d0570876c61ab9e520d776c38acbbb5b05a776d3f9ff98a5c8fd5162a444cf19" + "sha256:a448b2f64d686155468037e1ace9f2d2199776e17f0a46610480d311f73e3472", + "sha256:dd505485549a7a552833da5e6063639d0d177c04f23bc3864e41e5dc5f612168" ], "markers": "python_version >= '3.8'", - "version": "==2.2.1" - }, - "websocket-client": { - "hashes": [ - "sha256:10e511ea3a8c744631d3bd77e61eb17ed09304c413ad42cf6ddfa4c7787e8fe6", - "sha256:f4c3d22fec12a2461427a29957ff07d35098ee2d976d3ba244e688b8b4057588" - ], - "markers": "python_version >= '3.8'", - "version": "==1.7.0" - }, - "xlrd": { - "hashes": [ - "sha256:6a33ee89877bd9abc1158129f6e94be74e2679636b8a205b43b85206c3f0bbdd", - "sha256:f72f148f54442c6b056bf931dbc34f986fd0c3b0b6b5a58d013c9aef274d0c88" - ], - "version": "==2.0.1" - }, - "xlwt": { - "hashes": [ - "sha256:a082260524678ba48a297d922cc385f58278b8aa68741596a87de01a9c628b2e", - "sha256:c59912717a9b28f1a3c2a98fd60741014b06b043936dcecbc113eaaada156c88" - ], - "version": "==1.3.0" + "version": "==2.2.2" }, "zipp": { "hashes": [ - "sha256:206f5a15f2af3dbaee80769fb7dc6f249695e940acca08dfb2a4769fe61e538b", - "sha256:2884ed22e7d8961de1c9a05142eb69a247f120291bc0206a00a7642f09b5b715" + "sha256:bf1dcf6450f873a13e952a29504887c89e6de7506209e5b1bcc3460135d4de19", + "sha256:f091755f667055f2d02b32c53771a7a6c8b47e1fdbc4b72a8b9072b3eef8015c" ], "markers": "python_version >= '3.8'", - "version": "==3.18.1" + "version": "==3.19.2" } }, "develop": { @@ -925,40 +698,40 @@ }, "black": { "hashes": [ - "sha256:1bb9ca06e556a09f7f7177bc7cb604e5ed2d2df1e9119e4f7d2f1f7071c32e5d", - "sha256:21f9407063ec71c5580b8ad975653c66508d6a9f57bd008bb8691d273705adcd", - "sha256:4396ca365a4310beef84d446ca5016f671b10f07abdba3e4e4304218d2c71d33", - "sha256:44d99dfdf37a2a00a6f7a8dcbd19edf361d056ee51093b2445de7ca09adac965", - "sha256:5cd5b4f76056cecce3e69b0d4c228326d2595f506797f40b9233424e2524c070", - "sha256:64578cf99b6b46a6301bc28bdb89f9d6f9b592b1c5837818a177c98525dbe397", - "sha256:64e60a7edd71fd542a10a9643bf369bfd2644de95ec71e86790b063aa02ff745", - "sha256:652e55bb722ca026299eb74e53880ee2315b181dfdd44dca98e43448620ddec1", - "sha256:6644f97a7ef6f401a150cca551a1ff97e03c25d8519ee0bbc9b0058772882665", - "sha256:6ad001a9ddd9b8dfd1b434d566be39b1cd502802c8d38bbb1ba612afda2ef436", - "sha256:71d998b73c957444fb7c52096c3843875f4b6b47a54972598741fe9a7f737fcb", - "sha256:74eb9b5420e26b42c00a3ff470dc0cd144b80a766128b1771d07643165e08d0e", - "sha256:75a2d0b4f5eb81f7eebc31f788f9830a6ce10a68c91fbe0fade34fff7a2836e6", - "sha256:7852b05d02b5b9a8c893ab95863ef8986e4dda29af80bbbda94d7aee1abf8702", - "sha256:7f2966b9b2b3b7104fca9d75b2ee856fe3fdd7ed9e47c753a4bb1a675f2caab8", - "sha256:8e5537f456a22cf5cfcb2707803431d2feeb82ab3748ade280d6ccd0b40ed2e8", - "sha256:d4e71cdebdc8efeb6deaf5f2deb28325f8614d48426bed118ecc2dcaefb9ebf3", - "sha256:dae79397f367ac8d7adb6c779813328f6d690943f64b32983e896bcccd18cbad", - "sha256:e3a3a092b8b756c643fe45f4624dbd5a389f770a4ac294cf4d0fce6af86addaf", - "sha256:eb949f56a63c5e134dfdca12091e98ffb5fd446293ebae123d10fc1abad00b9e", - "sha256:f07b69fda20578367eaebbd670ff8fc653ab181e1ff95d84497f9fa20e7d0641", - "sha256:f95cece33329dc4aa3b0e1a771c41075812e46cf3d6e3f1dfe3d91ff09826ed2" + "sha256:257d724c2c9b1660f353b36c802ccece186a30accc7742c176d29c146df6e474", + "sha256:37aae07b029fa0174d39daf02748b379399b909652a806e5708199bd93899da1", + "sha256:415e686e87dbbe6f4cd5ef0fbf764af7b89f9057b97c908742b6008cc554b9c0", + "sha256:48a85f2cb5e6799a9ef05347b476cce6c182d6c71ee36925a6c194d074336ef8", + "sha256:7768a0dbf16a39aa5e9a3ded568bb545c8c2727396d063bbaf847df05b08cd96", + "sha256:7e122b1c4fb252fd85df3ca93578732b4749d9be076593076ef4d07a0233c3e1", + "sha256:88c57dc656038f1ab9f92b3eb5335ee9b021412feaa46330d5eba4e51fe49b04", + "sha256:8e537d281831ad0e71007dcdcbe50a71470b978c453fa41ce77186bbe0ed6021", + "sha256:98e123f1d5cfd42f886624d84464f7756f60ff6eab89ae845210631714f6db94", + "sha256:accf49e151c8ed2c0cdc528691838afd217c50412534e876a19270fea1e28e2d", + "sha256:b1530ae42e9d6d5b670a34db49a94115a64596bc77710b1d05e9801e62ca0a7c", + "sha256:b9176b9832e84308818a99a561e90aa479e73c523b3f77afd07913380ae2eab7", + "sha256:bdde6f877a18f24844e381d45e9947a49e97933573ac9d4345399be37621e26c", + "sha256:be8bef99eb46d5021bf053114442914baeb3649a89dc5f3a555c88737e5e98fc", + "sha256:bf10f7310db693bb62692609b397e8d67257c55f949abde4c67f9cc574492cc7", + "sha256:c872b53057f000085da66a19c55d68f6f8ddcac2642392ad3a355878406fbd4d", + "sha256:d36ed1124bb81b32f8614555b34cc4259c3fbc7eec17870e8ff8ded335b58d8c", + "sha256:da33a1a5e49c4122ccdfd56cd021ff1ebc4a1ec4e2d01594fef9b6f267a9e741", + "sha256:dd1b5a14e417189db4c7b64a6540f31730713d173f0b63e55fabd52d61d8fdce", + "sha256:e151054aa00bad1f4e1f04919542885f89f5f7d086b8a59e5000e6c616896ffb", + "sha256:eaea3008c281f1038edb473c1aa8ed8143a5535ff18f978a318f10302b254063", + "sha256:ef703f83fc32e131e9bcc0a5094cfe85599e7109f896fe8bc96cc402f3eb4b6e" ], "index": "pypi", "markers": "python_version >= '3.8'", - "version": "==24.4.0" + "version": "==24.4.2" }, "certifi": { "hashes": [ - "sha256:0569859f95fc761b18b45ef421b1290a0f65f147e92a1e5eb3e635f9a5e4e66f", - "sha256:dc383c07b76109f368f6106eee2b593b04a011ea4d55f652c6ca24a754d1cdd1" + "sha256:5a1e7645bc0ec61a09e26c36f6106dd4cf40c6db3a1fb6352b0244e7fb057c7b", + "sha256:c198e21b1289c2ab85ee4e67bb4b4ef3ead0892059901a8d5b622f24a1101e90" ], "markers": "python_version >= '3.6'", - "version": "==2024.2.2" + "version": "==2024.7.4" }, "charset-normalizer": { "hashes": [ @@ -1069,69 +842,61 @@ "toml" ], "hashes": [ - "sha256:00838a35b882694afda09f85e469c96367daa3f3f2b097d846a7216993d37f4c", - "sha256:0513b9508b93da4e1716744ef6ebc507aff016ba115ffe8ecff744d1322a7b63", - "sha256:09c3255458533cb76ef55da8cc49ffab9e33f083739c8bd4f58e79fecfe288f7", - "sha256:09ef9199ed6653989ebbcaacc9b62b514bb63ea2f90256e71fea3ed74bd8ff6f", - "sha256:09fa497a8ab37784fbb20ab699c246053ac294d13fc7eb40ec007a5043ec91f8", - "sha256:0f9f50e7ef2a71e2fae92774c99170eb8304e3fdf9c8c3c7ae9bab3e7229c5cf", - "sha256:137eb07173141545e07403cca94ab625cc1cc6bc4c1e97b6e3846270e7e1fea0", - "sha256:1f384c3cc76aeedce208643697fb3e8437604b512255de6d18dae3f27655a384", - "sha256:201bef2eea65e0e9c56343115ba3814e896afe6d36ffd37bab783261db430f76", - "sha256:38dd60d7bf242c4ed5b38e094baf6401faa114fc09e9e6632374388a404f98e7", - "sha256:3b799445b9f7ee8bf299cfaed6f5b226c0037b74886a4e11515e569b36fe310d", - "sha256:3ea79bb50e805cd6ac058dfa3b5c8f6c040cb87fe83de10845857f5535d1db70", - "sha256:40209e141059b9370a2657c9b15607815359ab3ef9918f0196b6fccce8d3230f", - "sha256:41c9c5f3de16b903b610d09650e5e27adbfa7f500302718c9ffd1c12cf9d6818", - "sha256:54eb8d1bf7cacfbf2a3186019bcf01d11c666bd495ed18717162f7eb1e9dd00b", - "sha256:598825b51b81c808cb6f078dcb972f96af96b078faa47af7dfcdf282835baa8d", - "sha256:5fc1de20b2d4a061b3df27ab9b7c7111e9a710f10dc2b84d33a4ab25065994ec", - "sha256:623512f8ba53c422fcfb2ce68362c97945095b864cda94a92edbaf5994201083", - "sha256:690db6517f09336559dc0b5f55342df62370a48f5469fabf502db2c6d1cffcd2", - "sha256:69eb372f7e2ece89f14751fbcbe470295d73ed41ecd37ca36ed2eb47512a6ab9", - "sha256:73bfb9c09951125d06ee473bed216e2c3742f530fc5acc1383883125de76d9cd", - "sha256:742a76a12aa45b44d236815d282b03cfb1de3b4323f3e4ec933acfae08e54ade", - "sha256:7c95949560050d04d46b919301826525597f07b33beba6187d04fa64d47ac82e", - "sha256:8130a2aa2acb8788e0b56938786c33c7c98562697bf9f4c7d6e8e5e3a0501e4a", - "sha256:8a2b2b78c78293782fd3767d53e6474582f62443d0504b1554370bde86cc8227", - "sha256:8ce1415194b4a6bd0cdcc3a1dfbf58b63f910dcb7330fe15bdff542c56949f87", - "sha256:9ca28a302acb19b6af89e90f33ee3e1906961f94b54ea37de6737b7ca9d8827c", - "sha256:a4cdc86d54b5da0df6d3d3a2f0b710949286094c3a6700c21e9015932b81447e", - "sha256:aa5b1c1bfc28384f1f53b69a023d789f72b2e0ab1b3787aae16992a7ca21056c", - "sha256:aadacf9a2f407a4688d700e4ebab33a7e2e408f2ca04dbf4aef17585389eff3e", - "sha256:ae71e7ddb7a413dd60052e90528f2f65270aad4b509563af6d03d53e979feafd", - "sha256:b14706df8b2de49869ae03a5ccbc211f4041750cd4a66f698df89d44f4bd30ec", - "sha256:b1a93009cb80730c9bca5d6d4665494b725b6e8e157c1cb7f2db5b4b122ea562", - "sha256:b2991665420a803495e0b90a79233c1433d6ed77ef282e8e152a324bbbc5e0c8", - "sha256:b2c5edc4ac10a7ef6605a966c58929ec6c1bd0917fb8c15cb3363f65aa40e677", - "sha256:b4d33f418f46362995f1e9d4f3a35a1b6322cb959c31d88ae56b0298e1c22357", - "sha256:b91cbc4b195444e7e258ba27ac33769c41b94967919f10037e6355e998af255c", - "sha256:c74880fc64d4958159fbd537a091d2a585448a8f8508bf248d72112723974cbd", - "sha256:c901df83d097649e257e803be22592aedfd5182f07b3cc87d640bbb9afd50f49", - "sha256:cac99918c7bba15302a2d81f0312c08054a3359eaa1929c7e4b26ebe41e9b286", - "sha256:cc4f1358cb0c78edef3ed237ef2c86056206bb8d9140e73b6b89fbcfcbdd40e1", - "sha256:ccd341521be3d1b3daeb41960ae94a5e87abe2f46f17224ba5d6f2b8398016cf", - "sha256:ce4b94265ca988c3f8e479e741693d143026632672e3ff924f25fab50518dd51", - "sha256:cf271892d13e43bc2b51e6908ec9a6a5094a4df1d8af0bfc360088ee6c684409", - "sha256:d5ae728ff3b5401cc320d792866987e7e7e880e6ebd24433b70a33b643bb0384", - "sha256:d71eec7d83298f1af3326ce0ff1d0ea83c7cb98f72b577097f9083b20bdaf05e", - "sha256:d898fe162d26929b5960e4e138651f7427048e72c853607f2b200909794ed978", - "sha256:d89d7b2974cae412400e88f35d86af72208e1ede1a541954af5d944a8ba46c57", - "sha256:dfa8fe35a0bb90382837b238fff375de15f0dcdb9ae68ff85f7a63649c98527e", - "sha256:e0be5efd5127542ef31f165de269f77560d6cdef525fffa446de6f7e9186cfb2", - "sha256:fdfafb32984684eb03c2d83e1e51f64f0906b11e64482df3c5db936ce3839d48", - "sha256:ff7687ca3d7028d8a5f0ebae95a6e4827c5616b31a4ee1192bdfde697db110d4" + "sha256:0086cd4fc71b7d485ac93ca4239c8f75732c2ae3ba83f6be1c9be59d9e2c6382", + "sha256:01c322ef2bbe15057bc4bf132b525b7e3f7206f071799eb8aa6ad1940bcf5fb1", + "sha256:03cafe82c1b32b770a29fd6de923625ccac3185a54a5e66606da26d105f37dac", + "sha256:044a0985a4f25b335882b0966625270a8d9db3d3409ddc49a4eb00b0ef5e8cee", + "sha256:07ed352205574aad067482e53dd606926afebcb5590653121063fbf4e2175166", + "sha256:0d1b923fc4a40c5832be4f35a5dab0e5ff89cddf83bb4174499e02ea089daf57", + "sha256:0e7b27d04131c46e6894f23a4ae186a6a2207209a05df5b6ad4caee6d54a222c", + "sha256:1fad32ee9b27350687035cb5fdf9145bc9cf0a094a9577d43e909948ebcfa27b", + "sha256:289cc803fa1dc901f84701ac10c9ee873619320f2f9aff38794db4a4a0268d51", + "sha256:3c59105f8d58ce500f348c5b56163a4113a440dad6daa2294b5052a10db866da", + "sha256:46c3d091059ad0b9c59d1034de74a7f36dcfa7f6d3bde782c49deb42438f2450", + "sha256:482855914928c8175735a2a59c8dc5806cf7d8f032e4820d52e845d1f731dca2", + "sha256:49c76cdfa13015c4560702574bad67f0e15ca5a2872c6a125f6327ead2b731dd", + "sha256:4b03741e70fb811d1a9a1d75355cf391f274ed85847f4b78e35459899f57af4d", + "sha256:4bea27c4269234e06f621f3fac3925f56ff34bc14521484b8f66a580aacc2e7d", + "sha256:4d5fae0a22dc86259dee66f2cc6c1d3e490c4a1214d7daa2a93d07491c5c04b6", + "sha256:543ef9179bc55edfd895154a51792b01c017c87af0ebaae092720152e19e42ca", + "sha256:54dece71673b3187c86226c3ca793c5f891f9fc3d8aa183f2e3653da18566169", + "sha256:6379688fb4cfa921ae349c76eb1a9ab26b65f32b03d46bb0eed841fd4cb6afb1", + "sha256:65fa405b837060db569a61ec368b74688f429b32fa47a8929a7a2f9b47183713", + "sha256:6616d1c9bf1e3faea78711ee42a8b972367d82ceae233ec0ac61cc7fec09fa6b", + "sha256:6fe885135c8a479d3e37a7aae61cbd3a0fb2deccb4dda3c25f92a49189f766d6", + "sha256:7221f9ac9dad9492cecab6f676b3eaf9185141539d5c9689d13fd6b0d7de840c", + "sha256:76d5f82213aa78098b9b964ea89de4617e70e0d43e97900c2778a50856dac605", + "sha256:7792f0ab20df8071d669d929c75c97fecfa6bcab82c10ee4adb91c7a54055463", + "sha256:831b476d79408ab6ccfadaaf199906c833f02fdb32c9ab907b1d4aa0713cfa3b", + "sha256:9146579352d7b5f6412735d0f203bbd8d00113a680b66565e205bc605ef81bc6", + "sha256:9cc44bf0315268e253bf563f3560e6c004efe38f76db03a1558274a6e04bf5d5", + "sha256:a73d18625f6a8a1cbb11eadc1d03929f9510f4131879288e3f7922097a429f63", + "sha256:a8659fd33ee9e6ca03950cfdcdf271d645cf681609153f218826dd9805ab585c", + "sha256:a94925102c89247530ae1dab7dc02c690942566f22e189cbd53579b0693c0783", + "sha256:ad4567d6c334c46046d1c4c20024de2a1c3abc626817ae21ae3da600f5779b44", + "sha256:b2e16f4cd2bc4d88ba30ca2d3bbf2f21f00f382cf4e1ce3b1ddc96c634bc48ca", + "sha256:bbdf9a72403110a3bdae77948b8011f644571311c2fb35ee15f0f10a8fc082e8", + "sha256:beb08e8508e53a568811016e59f3234d29c2583f6b6e28572f0954a6b4f7e03d", + "sha256:c4cbe651f3904e28f3a55d6f371203049034b4ddbce65a54527a3f189ca3b390", + "sha256:c7b525ab52ce18c57ae232ba6f7010297a87ced82a2383b1afd238849c1ff933", + "sha256:ca5d79cfdae420a1d52bf177de4bc2289c321d6c961ae321503b2ca59c17ae67", + "sha256:cdab02a0a941af190df8782aafc591ef3ad08824f97850b015c8c6a8b3877b0b", + "sha256:d17c6a415d68cfe1091d3296ba5749d3d8696e42c37fca5d4860c5bf7b729f03", + "sha256:d39bd10f0ae453554798b125d2f39884290c480f56e8a02ba7a6ed552005243b", + "sha256:d4b3cd1ca7cd73d229487fa5caca9e4bc1f0bca96526b922d61053ea751fe791", + "sha256:d50a252b23b9b4dfeefc1f663c568a221092cbaded20a05a11665d0dbec9b8fb", + "sha256:da8549d17489cd52f85a9829d0e1d91059359b3c54a26f28bec2c5d369524807", + "sha256:dcd070b5b585b50e6617e8972f3fbbee786afca71b1936ac06257f7e178f00f6", + "sha256:ddaaa91bfc4477d2871442bbf30a125e8fe6b05da8a0015507bfbf4718228ab2", + "sha256:df423f351b162a702c053d5dddc0fc0ef9a9e27ea3f449781ace5f906b664428", + "sha256:dff044f661f59dace805eedb4a7404c573b6ff0cdba4a524141bc63d7be5c7fd", + "sha256:e7e128f85c0b419907d1f38e616c4f1e9f1d1b37a7949f44df9a73d5da5cd53c", + "sha256:ed8d1d1821ba5fc88d4a4f45387b65de52382fa3ef1f0115a4f7a20cdfab0e94", + "sha256:f2501d60d7497fd55e391f423f965bbe9e650e9ffc3c627d5f0ac516026000b8", + "sha256:f7db0b6ae1f96ae41afe626095149ecd1b212b424626175a6633c2999eaad45b" ], "markers": "python_version >= '3.8'", - "version": "==7.4.4" - }, - "defusedxml": { - "hashes": [ - "sha256:1bb3032db185915b62d7c6209c5a8792be6a32ab2fedacc84e01b52c51aa3e69", - "sha256:a352e7e428770286cc899e2542b6cdaedb2b4953ff269a210103ec58f6198a61" - ], - "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4'", - "version": "==0.7.1" + "version": "==7.6.0" }, "diff-match-patch": { "hashes": [ @@ -1151,11 +916,11 @@ }, "django-import-export": { "hashes": [ - "sha256:2eac09e8cec8670f36e24314760448011ad23c51e8fb930d55f50d0c3c926da0", - "sha256:4deabc557801d368093608c86fd0f4831bc9540e2ea41ca2f023e2efb3eb6f48" + "sha256:16ecc5a9f0df46bde6eb278a3e65ebda0ee1db55656f36440e9fb83f40ab85a3", + "sha256:730ae2443a02b1ba27d8dba078a27ae9123adfcabb78161b4f130843607b3df9" ], "markers": "python_version >= '3.8'", - "version": "==3.3.8" + "version": "==4.1.1" }, "django-selenium-clean": { "hashes": [ @@ -1174,21 +939,13 @@ "markers": "python_version >= '3.6' and python_version < '4.0'", "version": "==1.2.0" }, - "et-xmlfile": { - "hashes": [ - "sha256:8eb9e2bc2f8c97e37a2dc85a09ecdcdec9d8a396530a6d5a33b30b9a92da0c5c", - "sha256:a2ba85d1d6a74ef63837eed693bcb89c3f752169b0e3e7ae5b16ca5e1b3deada" - ], - "markers": "python_version >= '3.6'", - "version": "==1.1.0" - }, "exceptiongroup": { "hashes": [ - "sha256:4bfd3996ac73b41e9b9628b04e079f193850720ea5945fc96a08633c66912f14", - "sha256:91f5c769735f051a4290d52edd0858999b57e5876e9f85937691bd4c9fa3ed68" + "sha256:3111b9d131c238bec2f8f516e123e14ba243563fb135d3fe885990585aa7795b", + "sha256:47c2edf7c6738fafb49fd34290706d1a1a2f4d1c6df275526b62cbb4aa5393cc" ], "markers": "python_version < '3.11'", - "version": "==1.2.0" + "version": "==1.2.2" }, "execnet": { "hashes": [ @@ -1238,12 +995,6 @@ "markers": "python_full_version >= '3.8.0'", "version": "==5.13.2" }, - "markuppy": { - "hashes": [ - "sha256:1adee2c0a542af378fe84548ff6f6b0168f3cb7f426b46961038a2bcfaad0d5f" - ], - "version": "==1.14" - }, "mypy-extensions": { "hashes": [ "sha256:4392f6c0eb8a5668a69e23d168ffa70f0be9ccfd32b5cc2d26a34ae5b844552d", @@ -1252,20 +1003,6 @@ "markers": "python_version >= '3.5'", "version": "==1.0.0" }, - "odfpy": { - "hashes": [ - "sha256:db766a6e59c5103212f3cc92ec8dd50a0f3a02790233ed0b52148b70d3c438ec", - "sha256:fc3b8d1bc098eba4a0fda865a76d9d1e577c4ceec771426bcb169a82c5e9dfe0" - ], - "version": "==1.4.1" - }, - "openpyxl": { - "hashes": [ - "sha256:a6f5977418eff3b2d5500d54d9db50c8277a368436f4e4f8ddb1be3422870184", - "sha256:f91456ead12ab3c6c2e9491cf33ba6d08357d802192379bb482f1033ade496f5" - ], - "version": "==3.1.2" - }, "outcome": { "hashes": [ "sha256:9dcf02e65f2971b80047b377468e72a268e15c0af3cf1238e6ff14f7f91143b8", @@ -1276,11 +1013,11 @@ }, "packaging": { "hashes": [ - "sha256:2ddfb553fdf02fb784c234c7ba6ccc288296ceabec964ad2eae3777778130bc5", - "sha256:eb82c5e3e56209074766e6885bb04b8c38a0c015d0a30036ebe7ece34c9989e9" + "sha256:026ed72c8ed3fcce5bf8950572258698927fd1dbda10a5e981cdf0ac37f4f002", + "sha256:5b8f2217dbdbd2f7f384c41c628544e6d52f2d0f53c6d0c3ea61aa5d1d7ff124" ], - "markers": "python_version >= '3.7'", - "version": "==24.0" + "markers": "python_version >= '3.8'", + "version": "==24.1" }, "pathspec": { "hashes": [ @@ -1292,19 +1029,19 @@ }, "platformdirs": { "hashes": [ - "sha256:0614df2a2f37e1a662acbd8e2b25b92ccf8632929bc6d43467e17fe89c75e068", - "sha256:ef0cc731df711022c174543cb70a9b5bd22e5a9337c8624ef2c2ceb8ddad8768" + "sha256:2d7a1657e36a80ea911db832a8a6ece5ee53d8de21edd5cc5879af6530b1bfee", + "sha256:38b7b51f512eed9e84a22788b4bce1de17c0adb134d6becb09836e37d8654cd3" ], "markers": "python_version >= '3.8'", - "version": "==4.2.0" + "version": "==4.2.2" }, "pluggy": { "hashes": [ - "sha256:7db9f7b503d67d1c5b95f59773ebb58a8c1c288129a88665838012cfb07b8981", - "sha256:8c85c2876142a764e5b7548e7d9a0e0ddb46f5185161049a79b7e974454223be" + "sha256:2cffa88e94fdc978c4c574f15f9e59b7f4201d439195c3715ca9e2486f1d0cf1", + "sha256:44e1ad92c8ca002de6377e165f3e0f1be63266ab4d554740532335b9d75ea669" ], "markers": "python_version >= '3.8'", - "version": "==1.4.0" + "version": "==1.5.0" }, "pypdf2": { "hashes": [ @@ -1369,12 +1106,12 @@ }, "pytest-xdist": { "hashes": [ - "sha256:cbb36f3d67e0c478baa57fa4edc8843887e0f6cfc42d677530a36d7472b32d8a", - "sha256:d075629c7e00b611df89f490a5063944bee7a4362a5ff11c7cc7824a03dfce24" + "sha256:9ed4adfb68a016610848639bb7e02c9352d5d9f03d04809919e2dafc3be4cca7", + "sha256:ead156a4db231eec769737f57668ef58a2084a34b2e55c4a8fa20d861107300d" ], "index": "pypi", - "markers": "python_version >= '3.7'", - "version": "==3.5.0" + "markers": "python_version >= '3.8'", + "version": "==3.6.1" }, "pytz": { "hashes": [ @@ -1391,48 +1128,13 @@ "index": "pypi", "version": "==3.0" }, - "pyyaml": { - "hashes": [ - "sha256:08682f6b72c722394747bddaf0aa62277e02557c0fd1c42cb853016a38f8dedf", - "sha256:0f5f5786c0e09baddcd8b4b45f20a7b5d61a7e7e99846e3c799b05c7c53fa696", - "sha256:129def1b7c1bf22faffd67b8f3724645203b79d8f4cc81f674654d9902cb4393", - "sha256:294db365efa064d00b8d1ef65d8ea2c3426ac366c0c4368d930bf1c5fb497f77", - "sha256:3b2b1824fe7112845700f815ff6a489360226a5609b96ec2190a45e62a9fc922", - "sha256:3bd0e463264cf257d1ffd2e40223b197271046d09dadf73a0fe82b9c1fc385a5", - "sha256:4465124ef1b18d9ace298060f4eccc64b0850899ac4ac53294547536533800c8", - "sha256:49d4cdd9065b9b6e206d0595fee27a96b5dd22618e7520c33204a4a3239d5b10", - "sha256:4e0583d24c881e14342eaf4ec5fbc97f934b999a6828693a99157fde912540cc", - "sha256:5accb17103e43963b80e6f837831f38d314a0495500067cb25afab2e8d7a4018", - "sha256:607774cbba28732bfa802b54baa7484215f530991055bb562efbed5b2f20a45e", - "sha256:6c78645d400265a062508ae399b60b8c167bf003db364ecb26dcab2bda048253", - "sha256:72a01f726a9c7851ca9bfad6fd09ca4e090a023c00945ea05ba1638c09dc3347", - "sha256:74c1485f7707cf707a7aef42ef6322b8f97921bd89be2ab6317fd782c2d53183", - "sha256:895f61ef02e8fed38159bb70f7e100e00f471eae2bc838cd0f4ebb21e28f8541", - "sha256:8c1be557ee92a20f184922c7b6424e8ab6691788e6d86137c5d93c1a6ec1b8fb", - "sha256:bb4191dfc9306777bc594117aee052446b3fa88737cd13b7188d0e7aa8162185", - "sha256:bfb51918d4ff3d77c1c856a9699f8492c612cde32fd3bcd344af9be34999bfdc", - "sha256:c20cfa2d49991c8b4147af39859b167664f2ad4561704ee74c1de03318e898db", - "sha256:cb333c16912324fd5f769fff6bc5de372e9e7a202247b48870bc251ed40239aa", - "sha256:d2d9808ea7b4af864f35ea216be506ecec180628aced0704e34aca0b040ffe46", - "sha256:d483ad4e639292c90170eb6f7783ad19490e7a8defb3e46f97dfe4bacae89122", - "sha256:dd5de0646207f053eb0d6c74ae45ba98c3395a571a2891858e87df7c9b9bd51b", - "sha256:e1d4970ea66be07ae37a3c2e48b5ec63f7ba6804bdddfdbd3cfd954d25a82e63", - "sha256:e4fac90784481d221a8e4b1162afa7c47ed953be40d31ab4629ae917510051df", - "sha256:fa5ae20527d8e831e8230cbffd9f8fe952815b2b7dae6ffec25318803a7528fc", - "sha256:fd7f6999a8070df521b6384004ef42833b9bd62cfee11a09bda1079b4b704247", - "sha256:fdc842473cd33f45ff6bce46aea678a54e3d21f1b61a7750ce3c498eedfe25d6", - "sha256:fe69978f3f768926cfa37b867e3843918e012cf83f680806599ddce33c2c68b0" - ], - "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4, 3.5'", - "version": "==5.4.1" - }, "requests": { "hashes": [ - "sha256:58cd2187c01e70e6e26505bca751777aa9f2ee0b7f4300988b709f44e013003f", - "sha256:942c5a758f98d790eaed1a29cb6eefc7ffb0d1cf7af05c3d2791656dbd6ad1e1" + "sha256:dd951ff5ecf3e3b3aa26b40703ba77495dab41da839ae72ef3c8e5d8e2433289", + "sha256:fc06670dd0ed212426dfeb94fc1b983d917c4f9847c863f313c9dfaaffb7c23c" ], - "markers": "python_version >= '3.7'", - "version": "==2.31.0" + "markers": "python_version >= '3.8'", + "version": "==2.32.2" }, "responses": { "hashes": [ @@ -1457,7 +1159,7 @@ "sha256:1e61c37477a1626458e36f7b1d82aa5c9b094fa4802892072e49de9c60c4c926", "sha256:8abb2f1d86890a2dfb989f9a77cfcfd3e47c2a354b01111771326f8aa26e0254" ], - "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'", + "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2'", "version": "==1.16.0" }, "snapshottest": { @@ -1485,20 +1187,13 @@ }, "sqlparse": { "hashes": [ - "sha256:714d0a4932c059d16189f58ef5411ec2287a4360f17cdd0edd2d09d4c5087c93", - "sha256:c204494cd97479d0e39f28c93d46c0b2d5959c7b9ab904762ea6c7af211c8663" + "sha256:773dcbf9a5ab44a090f3441e2180efe2560220203dc2f8c0b0fa141e18b505e4", + "sha256:bb6b4df465655ef332548e24f08e205afc81b9ab86cb1c45657a7ff173a3a00e" ], "markers": "python_version >= '3.8'", - "version": "==0.5.0" + "version": "==0.5.1" }, "tablib": { - "extras": [ - "html", - "ods", - "xls", - "xlsx", - "yaml" - ], "hashes": [ "sha256:9821caa9eca6062ff7299fa645e737aecff982e6b2b42046928a6413c8dabfd9", "sha256:f6661dfc45e1d4f51fa8a6239f9c8349380859a5bfaa73280645f046d6c96e33" @@ -1524,11 +1219,11 @@ }, "trio": { "hashes": [ - "sha256:9b41f5993ad2c0e5f62d0acca320ec657fdb6b2a2c22b8c7aed6caf154475c4e", - "sha256:e6458efe29cc543e557a91e614e2b51710eba2961669329ce9c862d50c6e8e81" + "sha256:67c5ec3265dd4abc7b1d1ab9ca4fe4c25b896f9c93dac73713778adab487f9c4", + "sha256:bb9c1b259591af941fccfbabbdc65bc7ed764bd2db76428454c894cd5e3d2032" ], "markers": "python_version >= '3.8'", - "version": "==0.25.0" + "version": "==0.26.0" }, "trio-websocket": { "hashes": [ @@ -1540,19 +1235,19 @@ }, "typing-extensions": { "hashes": [ - "sha256:83f085bd5ca59c80295fc2a82ab5dac679cbe02b9f33f7d83af68e241bea51b0", - "sha256:c1f94d72897edaf4ce775bb7558d5b79d8126906a14ea5ed1635921406c0387a" + "sha256:04e5ca0351e0f3f85c6853954072df659d0d13fac324d0072316b67d7794700d", + "sha256:1a7ead55c7e559dd4dee8856e3a88b41225abfe1ce8df57b7c13915fe121ffb8" ], "markers": "python_version >= '3.8'", - "version": "==4.11.0" + "version": "==4.12.2" }, "urllib3": { "hashes": [ - "sha256:450b20ec296a467077128bff42b73080516e71b56ff59a60a02bef2232c4fa9d", - "sha256:d0570876c61ab9e520d776c38acbbb5b05a776d3f9ff98a5c8fd5162a444cf19" + "sha256:a448b2f64d686155468037e1ace9f2d2199776e17f0a46610480d311f73e3472", + "sha256:dd505485549a7a552833da5e6063639d0d177c04f23bc3864e41e5dc5f612168" ], "markers": "python_version >= '3.8'", - "version": "==2.2.1" + "version": "==2.2.2" }, "wasmer": { "hashes": [ @@ -1599,20 +1294,6 @@ ], "markers": "python_full_version >= '3.7.0'", "version": "==1.2.0" - }, - "xlrd": { - "hashes": [ - "sha256:6a33ee89877bd9abc1158129f6e94be74e2679636b8a205b43b85206c3f0bbdd", - "sha256:f72f148f54442c6b056bf931dbc34f986fd0c3b0b6b5a58d013c9aef274d0c88" - ], - "version": "==2.0.1" - }, - "xlwt": { - "hashes": [ - "sha256:a082260524678ba48a297d922cc385f58278b8aa68741596a87de01a9c628b2e", - "sha256:c59912717a9b28f1a3c2a98fd60741014b06b043936dcecbc113eaaada156c88" - ], - "version": "==1.3.0" } } } diff --git a/cfl_common/MANIFEST.in b/cfl_common/MANIFEST.in index fd08209a2..b1e7ec051 100644 --- a/cfl_common/MANIFEST.in +++ b/cfl_common/MANIFEST.in @@ -1,3 +1,2 @@ -include common/fixtures/*.json graft common/templates graft common/static diff --git a/cfl_common/common/csp_config.py b/cfl_common/common/csp_config.py index aea403f59..b55180d1d 100644 --- a/cfl_common/common/csp_config.py +++ b/cfl_common/common/csp_config.py @@ -15,8 +15,6 @@ "https://www.google-analytics.com/", "https://pyodide-cdn2.iodide.io/v0.15.0/full/", "https://crowdin.com/", - f"wss://{MODULE_NAME}-aimmo.codeforlife.education/", - f"https://{MODULE_NAME}-aimmo.codeforlife.education/", ) CSP_FONT_SRC = ("'self'", "https://fonts.gstatic.com/", "https://fonts.googleapis.com/", "https://use.typekit.net/") CSP_SCRIPT_SRC = ( diff --git a/cfl_common/common/migrations/0005_add_worksheets.py b/cfl_common/common/migrations/0005_add_worksheets.py index 856fcc8f3..da1cb8355 100644 --- a/cfl_common/common/migrations/0005_add_worksheets.py +++ b/cfl_common/common/migrations/0005_add_worksheets.py @@ -1,12 +1,8 @@ -from common.helpers.data_migration_loader import load_data_from_file from django.db import migrations class Migration(migrations.Migration): - dependencies = [ - ("common", "0004_add_aimmocharacters"), - ("aimmo", "0020_add_info_to_worksheet"), - ] + dependencies = [("common", "0004_add_aimmocharacters")] operations = [migrations.RunPython(migrations.RunPython.noop, reverse_code=migrations.RunPython.noop)] diff --git a/cfl_common/common/migrations/0007_add_pdf_names_to_first_two_worksheets.py b/cfl_common/common/migrations/0007_add_pdf_names_to_first_two_worksheets.py index 7af0cde05..56bbf61f3 100644 --- a/cfl_common/common/migrations/0007_add_pdf_names_to_first_two_worksheets.py +++ b/cfl_common/common/migrations/0007_add_pdf_names_to_first_two_worksheets.py @@ -1,12 +1,8 @@ -from common.helpers.data_migration_loader import load_data_from_file from django.db import migrations class Migration(migrations.Migration): - dependencies = [ - ("common", "0006_update_aimmo_character_image_path"), - ("aimmo", "0021_add_pdf_names_to_worksheet"), - ] + dependencies = [("common", "0006_update_aimmo_character_image_path")] operations = [migrations.RunPython(migrations.RunPython.noop, reverse_code=migrations.RunPython.noop)] diff --git a/cfl_common/common/migrations/0054_delete_aimmo_models.py b/cfl_common/common/migrations/0054_delete_aimmo_models.py new file mode 100644 index 000000000..c9956e5c0 --- /dev/null +++ b/cfl_common/common/migrations/0054_delete_aimmo_models.py @@ -0,0 +1,20 @@ +# Generated by Django 3.2.25 on 2024-07-31 00:13 + +from django.db import migrations + + +class Migration(migrations.Migration): + + dependencies = [ + ('common', '0053_clean_class_data'), + ] + + operations = [ + migrations.DeleteModel( + name='AimmoCharacter', + ), + migrations.RemoveField( + model_name='userprofile', + name='aimmo_badges', + ), + ] diff --git a/cfl_common/common/models.py b/cfl_common/common/models.py index e927778a5..bed7dc44b 100644 --- a/cfl_common/common/models.py +++ b/cfl_common/common/models.py @@ -16,14 +16,6 @@ class UserProfile(models.Model): developer = models.BooleanField(default=False) is_verified = models.BooleanField(default=False) - # Holds the user's earned kurono badges. This information has to be on the - # UserProfile as the Avatar objects are deleted every time the Game gets - # deleted. - # This is a string showing which badges in which worksheets have been - # earned. The format is "X:Y" where X is the worksheet ID and Y is the - # badge ID. This repeats for all badges and each pair is comma-separated. - aimmo_badges = models.CharField(max_length=200, null=True, blank=True) - # TODO: Make not nullable once data has been transferred first_name = models.CharField(max_length=200, null=True, blank=True) _first_name = models.BinaryField(null=True, blank=True) @@ -387,23 +379,6 @@ def stripStudentName(name): return re.sub("[ \t]+", " ", name.strip()) -class AimmoCharacterManager(models.Manager): - def sorted(self): - return self.get_queryset().order_by("sort_order") - - -class AimmoCharacter(models.Model): - name = models.CharField(max_length=255) - description = models.TextField() - image_path = models.CharField(max_length=255) - sort_order = models.IntegerField() - alt = models.CharField(max_length=255, null=True) - objects = AimmoCharacterManager() - - def __str__(self) -> str: - return self.name - - # ----------------------------------------------------------------------- # Below are models used for data tracking and maintenance # ----------------------------------------------------------------------- diff --git a/cfl_common/common/tests/test_migration_aimmo_characters.py b/cfl_common/common/tests/test_migration_aimmo_characters.py deleted file mode 100644 index 4ada45931..000000000 --- a/cfl_common/common/tests/test_migration_aimmo_characters.py +++ /dev/null @@ -1,29 +0,0 @@ -import pytest -from django.db.models.query import QuerySet - - -@pytest.mark.django_db -def test_characters_added(migrator): - migrator.apply_initial_migration(("common", "0002_emailverification")) - new_state = migrator.apply_tested_migration(("common", "0004_add_aimmocharacters")) - - model_names = [model._meta.db_table for model in new_state.apps.get_models()] - - assert "common_aimmocharacter" in model_names - - AimmoCharacter = new_state.apps.get_model("common", "aimmocharacter") - all_characters: QuerySet = AimmoCharacter.objects.all() - - assert all_characters.count() == 3 - - -@pytest.mark.django_db -def test_image_paths_updated(migrator): - migrator.apply_initial_migration(("common", "0005_add_worksheets")) - new_state = migrator.apply_tested_migration(("common", "0006_update_aimmo_character_image_path")) - - AimmoCharacter = new_state.apps.get_model("common", "aimmocharacter") - all_characters: QuerySet = AimmoCharacter.objects.all() - - for character in all_characters: - assert character.image_path.startswith("images/aimmo_characters/") diff --git a/example_project/portal_test_settings.py b/example_project/portal_test_settings.py index 38e232184..0fe3ae5b6 100644 --- a/example_project/portal_test_settings.py +++ b/example_project/portal_test_settings.py @@ -83,7 +83,6 @@ SILENCED_SYSTEM_CHECKS = ["captcha.recaptcha_test_key_error"] INSTALLED_APPS = [ - "aimmo", "game", "pipeline", "portal", diff --git a/example_project/settings.py b/example_project/settings.py index a088cc625..ec62830be 100644 --- a/example_project/settings.py +++ b/example_project/settings.py @@ -39,7 +39,6 @@ SILENCED_SYSTEM_CHECKS = ["captcha.recaptcha_test_key_error"] INSTALLED_APPS = [ - "aimmo", "game", "pipeline", "portal", diff --git a/example_project/urls.py b/example_project/urls.py index 781b7836d..012948410 100644 --- a/example_project/urls.py +++ b/example_project/urls.py @@ -1,4 +1,3 @@ -from aimmo import urls as aimmo_urls from django.conf.urls import include, url from django.contrib import admin from django.urls import path @@ -12,5 +11,4 @@ url(r"^", include(portal_urls)), path("administration/", admin.site.urls), url(r"^rapidrouter/", include(game_urls)), - url(r"^kurono/", include(aimmo_urls)), ] diff --git a/portal/forms/add_game.py b/portal/forms/add_game.py deleted file mode 100644 index f8661c5a6..000000000 --- a/portal/forms/add_game.py +++ /dev/null @@ -1,29 +0,0 @@ -from aimmo.models import Game -from common.models import Class -from django.core.exceptions import ValidationError -from django.db.models.query import QuerySet -from django.forms import ModelChoiceField, ModelForm, Select - - -class AddGameForm(ModelForm): - def __init__(self, classes: QuerySet, *args, **kwargs): - super(AddGameForm, self).__init__(*args, **kwargs) - self.fields["game_class"].queryset = classes - - game_class = ModelChoiceField(queryset=None, widget=Select, label="Class", required=True) - - class Meta: - model = Game - fields = ["game_class"] - - def clean(self): - super(AddGameForm, self).clean() - game_class: Class = self.cleaned_data.get("game_class") - - if not game_class: - raise ValidationError("An invalid class was entered") - - if Game.objects.filter(game_class=game_class, is_archived=False).exists(): - raise ValidationError("An active game already exists for this class") - - return self.cleaned_data diff --git a/portal/static/portal/img/kurono_hero.jpg b/portal/static/portal/img/kurono_hero.jpg deleted file mode 100644 index 3d69c9337..000000000 Binary files a/portal/static/portal/img/kurono_hero.jpg and /dev/null differ diff --git a/portal/static/portal/img/kurono_landing_hero.png b/portal/static/portal/img/kurono_landing_hero.png deleted file mode 100644 index 9e39c8ef6..000000000 Binary files a/portal/static/portal/img/kurono_landing_hero.png and /dev/null differ diff --git a/portal/static/portal/img/kurono_logo.svg b/portal/static/portal/img/kurono_logo.svg deleted file mode 100644 index 26eda960a..000000000 --- a/portal/static/portal/img/kurono_logo.svg +++ /dev/null @@ -1 +0,0 @@ -K_simple_logo_full_colour \ No newline at end of file diff --git a/portal/static/portal/img/kurono_logo_grey_background.svg b/portal/static/portal/img/kurono_logo_grey_background.svg deleted file mode 100644 index 0a7f02b9d..000000000 --- a/portal/static/portal/img/kurono_logo_grey_background.svg +++ /dev/null @@ -1 +0,0 @@ - \ No newline at end of file diff --git a/portal/static/portal/img/kurono_logo_mark.svg b/portal/static/portal/img/kurono_logo_mark.svg deleted file mode 100644 index 4e3300da6..000000000 --- a/portal/static/portal/img/kurono_logo_mark.svg +++ /dev/null @@ -1 +0,0 @@ -K_mark_full_colour \ No newline at end of file diff --git a/portal/static/portal/img/kurono_resources_hero.jpg b/portal/static/portal/img/kurono_resources_hero.jpg deleted file mode 100644 index 3d6ad41d6..000000000 Binary files a/portal/static/portal/img/kurono_resources_hero.jpg and /dev/null differ diff --git a/portal/static/portal/img/kurono_story.png b/portal/static/portal/img/kurono_story.png deleted file mode 100644 index c08ee680b..000000000 Binary files a/portal/static/portal/img/kurono_story.png and /dev/null differ diff --git a/portal/static/portal/img/thumbnail_educate_kurono.png b/portal/static/portal/img/thumbnail_educate_kurono.png deleted file mode 100644 index 3c3f0e85c..000000000 Binary files a/portal/static/portal/img/thumbnail_educate_kurono.png and /dev/null differ diff --git a/portal/static/portal/img/thumbnail_kurono_resources.png b/portal/static/portal/img/thumbnail_kurono_resources.png deleted file mode 100644 index 0d01f7c68..000000000 Binary files a/portal/static/portal/img/thumbnail_kurono_resources.png and /dev/null differ diff --git a/portal/static/portal/img/thumbnail_play_kurono.png b/portal/static/portal/img/thumbnail_play_kurono.png deleted file mode 100644 index fbed933f7..000000000 Binary files a/portal/static/portal/img/thumbnail_play_kurono.png and /dev/null differ diff --git a/portal/static/portal/js/aimmoGame.js b/portal/static/portal/js/aimmoGame.js deleted file mode 100644 index 60a47952c..000000000 --- a/portal/static/portal/js/aimmoGame.js +++ /dev/null @@ -1,106 +0,0 @@ -/* global showPopupConfirmation */ -/* global hidePopupConfirmation */ - -function classesText(classes) { - return classes - .map( - (name, index) => - `${ - index === 0 - ? "" - : index === classes.length - 1 - ? " and " - : ", " - }${$("
").text(name).html()}` - ) - .join(""); -} - -function clickDeleteGames() { - let selectedGameIds = []; - let selectedClasses = []; - $("input[name='game_ids']:checked").each(function () { - selectedGameIds.push($(this).val()); - selectedClasses.push($(this).data("className")); - }); - - if (!selectedGameIds.length) { - return; - } - - let title = "Delete class games"; - let text = ` - `; - let confirmHandler = "deleteGames()"; - - showPopupConfirmation(title, text, confirmHandler); - let popup = $(".popup-wrapper"); - popup.data("gameIds", selectedGameIds); -} - -function deleteGames() { - let gameIds = $("#popup").data("gameIds"); - - $.ajax({ - url: "/kurono/api/games/delete_games/", - type: "POST", - data: { game_ids: gameIds }, - traditional: true, - headers: { - "X-CSRFToken": $("input[name=csrfmiddlewaretoken]").val(), - }, - success: function (data) { - hidePopupConfirmation(); - document.location.reload(true); - }, - }); -} - -function changeWorksheetConfirmation(gameID, className, worksheetID) { - let title = "Change Challenge"; - let text = - ""; - let confirmHandler = "changeWorksheet()"; - - showPopupConfirmation(title, text, confirmHandler); - let popup = $(".popup-wrapper"); - popup.data("gameId", gameID); - popup.data("worksheetId", worksheetID); - $(".popup__class-name").text(className); -} - -function changeWorksheet() { - let gameID = $("#popup").data("gameId"); - let worksheetID = $("#popup").data("worksheetId"); - - $.ajax({ - url: "/kurono/api/games/" + gameID + "/", - type: "PUT", - data: { worksheet_id: worksheetID }, - success: function (data) { - hidePopupConfirmation(); - document.location.reload(true); - }, - }); -} - -$(document).ready(function () { - // Handlers for the games checklist and select all checklist - $('[id^="game_"]').on("click", () => { - $("#gamesListToggle").prop("checked", - $('[id^="game_"]:checked').length === $('[id^="game_"]').length); - }); - - $("#gamesListToggle").on("click", () => { - $('[id^="game_"]').prop("checked", $("#gamesListToggle").is(":checked")); - }); -}); diff --git a/portal/static/portal/sass/partials/_banners.scss b/portal/static/portal/sass/partials/_banners.scss index 7e2f30b9a..08567d597 100644 --- a/portal/static/portal/sass/partials/_banners.scss +++ b/portal/static/portal/sass/partials/_banners.scss @@ -34,25 +34,6 @@ .banner--rapid-router--content { @include _padding(30px, 0px, 30px, 0px); } - - .banner--aimmo--content { - @include _padding(20 * $spacing, 0px, 20 * $spacing, 0px); - display: flex; - justify-content: center; - } -} - -.banner--aimmo-landing--content { - @include _padding(14 * $spacing, 0px, 14 * $spacing, 0px); - align-items: center; - display: flex; - flex-direction: column; - justify-content: center; - - .kurono-logo { - @include _padding(0px, 0px, 5 * $spacing, 0px); - width: 80vw; - } } .banner--picture { @@ -121,14 +102,6 @@ background-image: url("../img/teaching_resources_hero.jpg"); } -.banner--picture--aimmo { - background-image: url("../img/kurono_hero.jpg"); -} - -.banner--picture--aimmo-resources { - background-image: url("../img/kurono_resources_hero.jpg"); -} - .banner--homepage { display: flex; justify-content: center; @@ -241,86 +214,6 @@ } } -.banner--aimmo-home { - .dropdown { - width: 50%; - - button, - ul { - @include _margin(0px, 0px, 0px, 0px); - width: 100%; - z-index: $hover-content-level; - } - - li { - border-bottom: 1px solid $color-table-border; - } - - .dropdown-menu__option__text { - border-bottom: 1px solid transparent; - overflow: hidden; - text-overflow: ellipsis; - white-space: normal; - - &:hover { - border-bottom: 1px solid $color-text-gray; - } - } - - .glyphicon { - @include _font-size(10px); - @include _padding(8px, 0px, 0px, 0px); - } - } -} - -.banner--teacher--create-aimmo-game { - align-items: start; - - .kurono-logo { - @include _padding(20px, 0px, 25px, 0px); - display: flex; - flex-direction: column; - height: 100%; - justify-content: center; - - img { - width: 123 * $spacing; - } - } - - .button-group { - display: flex; - align-items: flex-end; - } - - #create-game-form { - padding: 0; - - .errorlist { - display: none; - } - - label { - color: white; - } - - input, - select { - height: auto; - margin: 0; - - &::placeholder { - color: $color-error; - } - } - } - - img { - @include _margin(14px, 0px, 0px, 0px); - } -} - .banner--rapid-router { background-image: url("../img/rapid_router_landing_hero.png"); background-position: center center; @@ -336,46 +229,6 @@ } } -.banner--aimmo { - background-image: url("../img/kurono_landing_hero.png"); - background-position: center center; - background-repeat: no-repeat; - background-size: cover; - - img { - @include _margin(60px, 0px, 60px, 0px); - width: 40%; - } - - &.banner img { - width: 25%; - } -} - -.banner--aimmo--video--container { - @include _padding(12 * $spacing, 0px, 12 * $spacing, 0px); - align-items: center; - display: flex; - justify-content: center; - position: relative; -} - -.banner--aimmo--video { - left: 0px; - height: 100%; - object-fit: cover; - overflow: hidden; - position: absolute; - top: 0px; - width: 100%; - z-index: $behind-content-level; -} - -.banner--aimmo--video--text { - @include _padding(10 * $spacing, 10 * $spacing, 10 * $spacing, 10 * $spacing); - color: $color-text-secondary; -} - .banner--resources { @include _padding(0px, 0px, 30px, 0px); align-items: center; @@ -428,14 +281,6 @@ } } -@media (min-width: $tablet-small-min-width) { - .banner--aimmo-landing--content { - .kurono-logo { - width: 615px; - } - } -} - @media (max-width: $tablet-small-min-width) { .banner--picture { margin: 0 auto 0 auto; @@ -464,28 +309,6 @@ } } - .banner--aimmo { - @include _padding(50px, 0px, 150px, 0px); - flex-direction: column; - - .button { - margin-bottom: 50px; - } - } - - .banner--aimmo-landing--content { - @include _padding(8 * $spacing, 8 * $spacing, 8 * $spacing, 8 * $spacing); - } - - .banner--aimmo--video { - max-height: 300px; - object-fit: none; - } - - .banner--aimmo--video--container { - @include _padding(0px, 0px, 0px, 0px); - } - .banner--game { .banner--game__text { @include _padding(10px, 50px, 30px, 50px); diff --git a/portal/static/portal/sass/partials/_buttons.scss b/portal/static/portal/sass/partials/_buttons.scss index b739c4ba1..7bba55d46 100644 --- a/portal/static/portal/sass/partials/_buttons.scss +++ b/portal/static/portal/sass/partials/_buttons.scss @@ -460,18 +460,6 @@ td .button--primary { } } - .grid-kurono-sessions__accordion-button { - .icon-drop-down:before { - content: "arrow_drop_down"; - } - - &[aria-expanded="true"] { - .icon-drop-down:before { - content: "arrow_drop_up"; - } - } - } - .button--menu__item--teacher { background-color: $color-background-teach; } diff --git a/portal/static/portal/sass/partials/_grids.scss b/portal/static/portal/sass/partials/_grids.scss index ab979b1c2..ac460ba02 100644 --- a/portal/static/portal/sass/partials/_grids.scss +++ b/portal/static/portal/sass/partials/_grids.scss @@ -179,37 +179,6 @@ $column--icon-width: 3%; } } - .grid-kurono-sessions { - display: grid; - grid-auto-rows: auto; - grid-template-columns: - 70% - auto; - - .button { - @include _margin(0px, 0px, $spacing * 3, 0px); - } - - div p { - width: 90%; - } - - & > * { - align-items: center; - border-bottom: 1px solid $color-table-border; - display: grid; - } - - .grid-icon { - max-height: 60%; - max-width: 60%; - } - - h6 { - @include _margin(20px, 0px, 10px, 0px); - } - } - .grid-resources { border-top: 1px solid $color-table-border; display: grid; @@ -292,26 +261,4 @@ $column--icon-width: 3%; "button3"; row-gap: 4 * $spacing; } - - .grid-kurono-sessions { - .grid-kurono-sessions__item { - border-bottom: 1px solid $color-table-border; - } - - .grid-kurono-sessions__accordion-button { - align-items: center; - display: flex; - justify-content: space-between; - - .button-group__icon { - width: -webkit-fill-available; - width: -moz-available; - } - } - - .button-group { - display: flex; - justify-content: center; - } - } } diff --git a/portal/static/portal/sass/partials/_text.scss b/portal/static/portal/sass/partials/_text.scss index 4712b70dc..672762fba 100644 --- a/portal/static/portal/sass/partials/_text.scss +++ b/portal/static/portal/sass/partials/_text.scss @@ -69,15 +69,6 @@ a:focus { font-weight: 600; } -.kurono-overview__paragraph { - @include _font-size(25px); - @include _line-height(35px); - - + .kurono-overview__paragraph { - @include _padding(8 * $spacing, 0px, 0px, 0px); - } -} - .grid-benefits__text1 { grid-area: text1; } @@ -243,4 +234,4 @@ blockquote { body { font-size: 14px; } -} \ No newline at end of file +} diff --git a/portal/static/portal/sass/partials/_videos.scss b/portal/static/portal/sass/partials/_videos.scss deleted file mode 100644 index 4c62f9c24..000000000 --- a/portal/static/portal/sass/partials/_videos.scss +++ /dev/null @@ -1,10 +0,0 @@ -@import 'base'; - -.video--aimmo--play-online { - margin-top: 4 * $spacing; - padding-left: 0px; - padding-right: 0px; - -webkit-box-shadow: 0px 4px 18px 0px $color-ui-widget-overlay; - -moz-box-shadow: 0px 4px 18px 0px $color-ui-widget-overlay; - box-shadow: 0px 4px 18px 0px $color-ui-widget-overlay; -} diff --git a/portal/static/portal/sass/styles.scss b/portal/static/portal/sass/styles.scss index 1c60faec8..274a5b03b 100644 --- a/portal/static/portal/sass/styles.scss +++ b/portal/static/portal/sass/styles.scss @@ -15,7 +15,6 @@ @import "partials/text"; @import "partials/ui-dialog"; @import "partials/utils"; -@import "partials/videos"; .content-footer-wrapper { display: flex; diff --git a/portal/static/portal/video/aimmo_play_now_background_video.mp4 b/portal/static/portal/video/aimmo_play_now_background_video.mp4 deleted file mode 100644 index ef4d9dabd..000000000 Binary files a/portal/static/portal/video/aimmo_play_now_background_video.mp4 and /dev/null differ diff --git a/portal/strings/play.py b/portal/strings/play.py index cbee42cbb..7d7d93ce5 100644 --- a/portal/strings/play.py +++ b/portal/strings/play.py @@ -14,6 +14,5 @@ "description": "Whether you’re a parent, teacher or a student, our games support " "and guide you, making learning to code great fun. Get started with Rapid Router " "designed for students new to coding. Rapid Router is where you will build up your " - "ability until you are ready to advance to Kurono, where you can test your skills " - "in Python.", + "ability.", } diff --git a/portal/strings/student_aimmo_dashboard.py b/portal/strings/student_aimmo_dashboard.py deleted file mode 100644 index 74dd426c0..000000000 --- a/portal/strings/student_aimmo_dashboard.py +++ /dev/null @@ -1,6 +0,0 @@ -AIMMO_DASHBOARD_BANNER = { - "title": "Kurono dashboard", - "subtitle": "Let the adventures begin! A collection of challenges await your " - "exploration. See how far you can get.", - "image_class": "banner--picture--aimmo", -} diff --git a/portal/strings/teacher_resources.py b/portal/strings/teacher_resources.py index b2e54a23d..962ba8c82 100644 --- a/portal/strings/teacher_resources.py +++ b/portal/strings/teacher_resources.py @@ -7,13 +7,3 @@ "image_description": "Credit: Annie Spratt, Unsplash", "alt": "Boy playing on ipad", } - -KURONO_RESOURCES_BANNER = { - "title": "Kurono Resources", - "subtitle": "We’ve created a comprehensive set of teaching materials to help you " - "teach students the UK National Computing Curriculum.", - "text": "", - "image_class": "banner--picture--aimmo-resources", - "alt": "Two teenage girls laughing whilst working on laptops", - "image_description": "Credit: Brooke Cagle, Unsplash", -} diff --git a/portal/templates/portal/about.html b/portal/templates/portal/about.html index 078224c8a..1e793a975 100644 --- a/portal/templates/portal/about.html +++ b/portal/templates/portal/about.html @@ -10,23 +10,25 @@ {% block content %}
-

Code For Life is a non profit initiative that delivers free, open-source games that help all students learn - computing.

+

Code For Life is a non-profit initiative that delivers free, + open-source games that help all students learn computing.

2014

-

The year that computing was added to the UK curriculum. We've been supporting teachers and students - ever since.

+

The year that computing was added to the UK curriculum. We've + been supporting teachers and students ever since.

>160

-

Countries are taking part, with the UK, the USA, Brazil, Australia and Canada as top locations for - schools signed up to CFL. Nearly 10,000 schools are registered globally.

+

Countries are taking part, with the UK, the USA, Brazil, + Australia and Canada as top locations for schools signed up + to CFL. Nearly 10,000 schools are registered globally.

>260,000

-

Active users so far, with numbers growing every day. In 2020 alone, close to 100,000 new people - subscribed to our resources.

+

Active users so far, with numbers growing every day. In 2020 + alone, close to 100,000 new people subscribed to our + resources.

@@ -36,18 +38,23 @@

>260,000

What is Code for Life?
-

Code for Life (CFL) is a free, easy-to-use resource that provides teaching and lesson plans, user - guides and engagement through our two fun coding games: Rapid Router and Kurono. These games are - specially designed for people learning computing for the first time.

-

The aim is to teach new coders the basic principles, to help them thrive in an increasingly digital - world. CFL is primarily designed for and tested by primary school teachers. Our games are aligned - with the UK's computing curriculum, so teachers can incorporate CFL into their lessons.

-

Anyone looking to get into coding can also do so using the games and resources. We opened CFL - resources to parents and the general public during the 2020 Covid-19 pandemic so that people - don't need to be part of a school to have access.

+

Code for Life (CFL) is a free, easy-to-use resource that + provides teaching and lesson plans, user guides and + engagement through our fun coding games and resources. + These games are specially designed for people learning + computing for the first time.

+

The aim is to teach new coders the basic principles, to help + them thrive in an increasingly digital world. CFL is + primarily designed for and tested by primary school + teachers. Our games are aligned with the UK's computing + curriculum, so teachers can incorporate CFL into their + lessons.

+

But anyone looking to get into coding can also do so using + the games and resources.

- What is Code for Life? + What is Code for Life?
@@ -55,20 +62,25 @@
What is Code for Life?
- Who is Ocado Group? + Who is Ocado Group?
Who is Ocado Group?
-

Ocado Group, the online grocery solutions provider, is powering the future of online retail. Ocado's - tech and solutions are supplied to grocery businesses all around the world. It enables these - forward-thinking retailers to do grocery online profitably, sustainably, and in a scalable manner.

-

The Ocado Smart Platform (OSP) is the world's most advanced end-to-end e-Commerce, fulfilment and - logistic platform.

+

Ocado Group, the online grocery solutions provider, is powering + the future of online retail. Ocado's tech and solutions are + supplied to grocery businesses all around the world. It enables + these forward-thinking retailers to do grocery online + profitably, sustainably, and in a scalable manner.

+

The Ocado Smart Platform (OSP) is the world's most advanced + end-to-end e-Commerce, fulfilment and logistic platform.

- Skills for the Future is one of Ocado Group's core Corporate Responsibility pillars, which is - part of the Ocado Unlimited strategy (alongside Natural Resources and Responsible Sourcing). For Ocado - Group, Skills for the Future means championing digital literacy. We want to inspire the next generation - of STEM leaders, so that everyone can fully participate in society.

+ Skills for the Future is one of Ocado Group's core Corporate + Responsibility pillars, which is part of the Ocado Unlimited + strategy (alongside Natural Resources and Responsible Sourcing). + For Ocado Group, Skills for the Future means championing digital + literacy. We want to inspire the next generation of STEM + leaders, so that everyone can fully participate in society.

@@ -79,21 +91,27 @@
Who is Ocado Group?

- “We were delighted computing entered the UK curriculum in 2014. However, many teachers felt - unprepared. And the lack of diversity in people studying STEM concerned us. So, we sought to - make the subject appeal to a broader group of both students and teachers." + “We were delighted computing entered the UK curriculum + in 2014. However, many teachers felt unprepared. And the + lack of diversity in people studying STEM concerned us. + So, we sought to make the subject appeal to a broader + group of both students and teachers."

-

Anne Marie Neatham, Commercial Director and COO Kindred, Ocado Group.

+

Anne Marie Neatham, Commercial Director and COO Kindred, + Ocado Group.

-

With that in mind, CFL was developed by volunteers and interns from Ocado Technology - the - technology arm of Ocado Group - and teacher Sharon Harrison, who created the Rapid Router learning - materials. Anne Marie continues:

+

With that in mind, CFL was developed by volunteers and + interns from Ocado Technology - the technology arm of Ocado + Group - and teacher Sharon Harrison, who created the Rapid + Router learning materials. Anne Marie continues:

- “I'm proud this initiative has been breaking down stereotypes. Children are seeing that you - don't have to fit a certain gender, race or personality type to get coding." + “I'm proud this initiative has been breaking down + stereotypes. Children are seeing that you don't have to + fit a certain gender, race or personality type to get + coding."

Today, CFL is operated by a small core team and volunteers.

@@ -107,29 +125,35 @@
Who is Ocado Group?
Our team and volunteers
-

Code for Life would not have been possible without the time and skills volunteered by our talented - developers and creatives at Ocado Technology. Thank you to everyone who has helped us get to where - we are now.

+

Code for Life would not have been possible without the time and + skills volunteered by our talented developers and creatives at + Ocado Technology. Thank you to everyone who has helped us get to + where we are now.

Want to get involved?
-

We are open source, so anyone can ask to contribute. You can play with game-running JavaScript, - Python/Django, animation using SVG and Raphael, and a lot more. We'd like input from all sorts of - backgrounds, whether you're: a programmer looking for a creative outlet; a teacher hoping to shape - the resources; or even a pupil putting your skills to the test.

+

We are open source, so anyone can ask to contribute. You can play + with game-running JavaScript, Python/Django, animation using SVG + and Raphael, and a lot more. We'd like input from all sorts of + backgrounds, whether you're: a programmer looking for a creative + outlet; a teacher hoping to shape the resources; or even a pupil + putting your skills to the test.

Developers
-

To contribute, head over to GitHub, - check out the issue tracker, and get started. There you can suggest new features or assign yourself - an issue to develop. You can find more info about how to do all these on our +

To contribute, head over toGitHub, + check out the issue tracker, and get started. There you can + suggest new features or assign yourself an issue to develop. You + can find more info about how to do all these on our docs on Gitbook.

Teachers, parents, and creatives
-

Please get in touch through our contact form and let us know how - you would like to get involved.

-

We would like to thank our friends who have contributed to this initiative.

+

Please get in touch through our contact + form and let us know how you would like to get involved.

+

We would like to thank our friends who have contributed to this + initiative.

-
We would like to thank our friends who have contributed to this initiative
+
We would like to thank our friends who have contributed to this + initiative
10x logo BCS logo @@ -140,10 +164,13 @@
We would like to thank our friends who have contributed to this initiative Pressure Cooker logo
-

10X, BCS Academy of Computing, Barefoot Computing, Computing at School, The National Museum of - Computing, Imperial College London, M&C Saatchi, Alvaro Ramirez, Jason Fingland, Ramneet Loyall, Sharon - Harrison, Keith Avery, Dale Coan, Rob Whitehouse, Mandy Nash, Tanya Nothard, Matt Trevor, Moy El-Bushra, - Richard Siwiak, Peter Tondrow, Liz Pratt, Pressure Cooker Studios, GAL Education, Hope Education.

+

10X, BCS Academy of Computing, Barefoot Computing, Computing + at School, The National Museum of Computing, Imperial College + London, M&C Saatchi, Alvaro Ramirez, Jason Fingland, Ramneet Loyall, + Sharon Harrison, Keith Avery, Dale Coan, Rob Whitehouse, Mandy Nash, + Tanya Nothard, Matt Trevor, Moy El-Bushra, Richard Siwiak, Peter + Tondrow, Liz Pratt, Pressure Cooker Studios, GAL Education, Hope + Education.

@@ -151,14 +178,18 @@
We would like to thank our friends who have contributed to this initiative

Dedicated to Sharon Harrison

- Sharon Harrison portrait + Sharon Harrison portrait
1956 — 2015
-

Sharon was instrumental in helping to create Code for Life. At the beginning of 2014 she was - recruited to act as our Educational Consultant. The project drew on her previous skills as a pioneering +

Sharon was instrumental in helping to create Code for Life. + At the beginning of 2014 she was recruited to act as our Educational + Consultant. The project drew on her previous skills as a pioneering computing teacher and education consultant.

-

Sharon has left a lasting legacy by creating something which will help teach STEM skills to the next - generation of computer scientists across the world.

+

Sharon has left a lasting legacy by creating something which will + help teach STEM skills to the next generation of computer scientists + across the world.

{% endblock content %} diff --git a/portal/templates/portal/contribute.html b/portal/templates/portal/contribute.html index 3b38dceab..dba91b981 100644 --- a/portal/templates/portal/contribute.html +++ b/portal/templates/portal/contribute.html @@ -12,30 +12,29 @@

How to get involved and gain experience

-
-
-

Code for Life would not have been possible without the dedication of our volunteers. -

-

In 2014, computing was added to the UK curriculum, requiring schools to teach coding - principles and programming foundations. -

-
-
-

Recognising a need to support teachers and students in navigating the uncharted territory, - Ocado Technology deployed an army of internal volunteers who worked after hours, fuelled by - free pizzas and fizzy drinks. -

-

What came out of this, is what you see today, used by educators and learners from over 180 - countries. Our products are open-source and free forever. -

-
+
+
+

Code for Life would not have been possible without the dedication + of our volunteers.

+

In 2014, computing was added to the UK curriculum, requiring + schools to teach coding principles and programming foundations.

-
- - Read our developer guide - +
+

Recognising a need to support teachers and students in navigating + the uncharted territory, Ocado Technology deployed an army of + internal volunteers who worked after hours, fuelled by free + pizzas and fizzy drinks.

+

What came out of this, is what you see today, used by educators + and learners from over 180 countries. Our products are + open-source and free forever.

+
@@ -44,26 +43,21 @@

How to get involved and gain experience

Our products

-

The portal or website

-

This is the gateway for users to get to know who we are and what we do. - It hosts our web-based games and plenty of teaching resources. -

+

The website

+

This is the gateway for users to get to know who we are and + what we do. It hosts our web-based games and plenty of + teaching resources, and the teacher dashboard for class + management.

Rapid Router

-

An introduction to coding that is aimed at Key Stages 1-3 (age 5 to 15). - Built on Blockly, it's a visual programming language similar to Scratch. - The levels start off with Blockly and gradually progress to Python. With over 100 levels, - Rapid Router is our flagship game with the biggest user base. -

- -

Kurono

-

A multiplayer game that is aimed at students at Key Stages 3 and up, - it is primarily for use in a class or a club setting. Students code in Python to - move their avatar around in order to complete tasks. Parts of Kurono are still in development. -

+

An introduction to coding that is aimed at ages 5 to 14. + Built on Blockly, it's a visual programming language similar + to Scratch. Rapid Router is our flagship game with the + biggest user base.

- Rapid Router + Rapid Router
@@ -72,22 +66,24 @@

Our products

- Gitbook + Gitbook

How you can contribute

-

Today, there is a small dedicated team working full time on Code for Life. - Our community, made up of dedicated teachers, tutors, students, and volunteers, - has played a vital part in Code For Life's growth. The resources and games are - aligned to the UK National Curriculum and have reached over 265,000 active users - across 180 countries, and over 600,000 users since the launch in 2014. We need your help to grow even more. -

-

- If contributing to open-source projects to support education in coding and technology - sounds exciting for you, we'd love to have you on board! -

+

Today, there is a small dedicated team working full time on Code + for Life. Our community, made up of dedicated teachers, tutors, + students, and volunteers, has played a vital part in Code For + Life's growth. The resources and games are aligned to the UK + National Curriculum and have reached over 260,000 active users + across 180 countries, and over 660,000 users since the launch in + 2014. We need your help to grow even more.

+

If contributing to open-source projects to support education in + coding and technology sounds exciting for you, we'd love to have + you on board!

diff --git a/portal/templates/portal/partials/aimmo_games_table.html b/portal/templates/portal/partials/aimmo_games_table.html deleted file mode 100644 index 56509c5f2..000000000 --- a/portal/templates/portal/partials/aimmo_games_table.html +++ /dev/null @@ -1,89 +0,0 @@ -{% load static %} -{% load app_tags %} -{% block scripts %} - -{% endblock scripts %} - -{% if open_play_games %} - {% include "portal/partials/popup.html" %} - - - - - - - - {% for game in open_play_games %} - - - - - - - {% endfor %} -
-

Class

-
-

Challenge

-
-

Action

-
- -
-
-

{{ game.game_class.name }} - {% if user.userprofile.teacher == game.game_class.teacher %} - (You) - {% else %} - ({{ game.game_class.teacher }}) - {% endif %} -

-
-
-
- -
-
-
- Play -
-
- -
- -{% else %} -

It doesn't look like you have any games created. To create a game, use the 'Select class' button above.

-{% endif %} diff --git a/portal/templates/portal/partials/header.html b/portal/templates/portal/partials/header.html index 31c6909ab..a200c2bf6 100644 --- a/portal/templates/portal/partials/header.html +++ b/portal/templates/portal/partials/header.html @@ -37,7 +37,6 @@ Games arrow_drop_down
@@ -78,9 +75,6 @@
{% if user|is_logged_in_as_school_user %} @@ -197,8 +191,6 @@

{{ user|make_into_username }}

{% else %} @@ -217,10 +209,6 @@

{{ user|make_into_username }}

Rapid Routerchevron_right - {% if not user|is_independent_student %} - Kuronochevron_right - {% endif %}
{% if user|is_logged_in_as_school_user %}
@@ -49,64 +57,43 @@
Starting with Blockly

Meet the characters

- Illustration of Wes + Illustration of Wes
Wes
-

Wes is as cunning as a fox, which is weird, because he's actually a wolf.

+

Wes is as cunning as a fox, which is weird, because he's actually + a wolf.

- Illustration of Kirsty + Illustration of Kirsty
Kirsty
-

Kirsty is a girl with big ambitions. Her biggest ambition is to take the crown, and rule the world!

+

Kirsty is a girl with big ambitions. Her biggest ambition is to + take the crown, and rule the world!

- Illustration of Dee + Illustration of Dee
Dee
-

Dee is a Mark II DeliviBot. She's super friendly and her wire hair sparks when she laughs.

+

Dee is a Mark II DeliviBot. She's super friendly and her wire + hair sparks when she laughs.

- Illustration of Nigel + Illustration of Nigel
Nigel
-

Nigel is the tallest kid in his class, and he's growing taller by the day.

+

Nigel is the tallest kid in his class, and he's growing taller by + the day.

- Illustration of Phil + Illustration of Phil
Phil
-

Phil is a Boarsnark, however, he is different to most Boarsnarks because he's very kind, and very gentle.

+

Phil is a Boarsnark, however, he is different to most Boarsnarks + because he's very kind, and very gentle.

-
- -
-
-
-
Progressing to Python
-

Kurono guides you and makes learning to code great fun.

-

Using Python, you can travel with your classmates through time to collect all the museum artefacts.

-

Ask your teacher or tutor to register.

-
-
- Login to get started -
-
-
- - Image of Kurono game with play button in bottom right corner - -
-
-
- -
-
-

Meet the characters

- {% character_list %} -
-
- -{% endblock scripts %} - -{% block subNav %} - - -{% endblock subNav %} - -{% block content %} -
-

My games

- {% games_table 'kurono/play' %} -
- -
-
-
-
-
-
-
Kurono Resources
-

We have a set of individual and collaborative worksheets that keep the students engaged and having fun whilst - embedding important Python skills, supported by lesson guides and resource sheets.

-

Please visit our dedicated Code for Life Space to find everything you need from lesson plans to solutions.

-

This space is only available to teachers.

-
- -
-
- Kurono Resources -
-
-
-
-
-
-
Tell us what you think of Kurono...
-

Your testing and feedback will help Code for Life deliver an enjoyable game, and will allow us to consult - with you on resources that will be relevant to teaching computing classes in secondary schools - (13 — 18 year olds).

- -
-
- -{% endblock content %} diff --git a/portal/templates/portal/ten_year_map.html b/portal/templates/portal/ten_year_map.html index 9eec66bca..0c7ccb915 100644 --- a/portal/templates/portal/ten_year_map.html +++ b/portal/templates/portal/ten_year_map.html @@ -1,6 +1,6 @@ {% extends 'portal/base.html' %} {% load static %} -{% load app_tags banner_tags headline_tags character_list_tags %} +{% load app_tags banner_tags headline_tags %} {% block subNav %} {% banner banner_name="BANNER" %} @@ -66,14 +66,14 @@
July 2024

- We completed the UK leg of our world tour with an enthusiastic reception from Year 4 students at Howe Dell Primary School - in Hatfield. The pupils were captivated by our presentation on different types of robots and Ocado Group's automation - technology used in sorting and packing deliveries. They were then tasked with designing robots to solve problems, producing - creative concepts like robot childminders and homework helpers. Midway through, we introduced them to coding with Rapid Router, - explaining algorithms and letting them experiment with the levels. The students were thrilled about the competition for the - best robot design, which included a prize of a golden 3D printed medal. Teachers praised the engaging and educational workshop, - while Ocado volunteers found the experience rewarding and beneficial for developing their communication and teaching skills. - The children's designs were exceptional, making the judging process challenging, but ultimately a few standout innovations were + We completed the UK leg of our world tour with an enthusiastic reception from Year 4 students at Howe Dell Primary School + in Hatfield. The pupils were captivated by our presentation on different types of robots and Ocado Group's automation + technology used in sorting and packing deliveries. They were then tasked with designing robots to solve problems, producing + creative concepts like robot childminders and homework helpers. Midway through, we introduced them to coding with Rapid Router, + explaining algorithms and letting them experiment with the levels. The students were thrilled about the competition for the + best robot design, which included a prize of a golden 3D printed medal. Teachers praised the engaging and educational workshop, + while Ocado volunteers found the experience rewarding and beneficial for developing their communication and teaching skills. + The children's designs were exceptional, making the judging process challenging, but ultimately a few standout innovations were selected.

''' - -snapshots['test_benefits 1'] = ''' - -
- - - - -
Test title
- - -
Test title
- - -
Test title
- -

Test text

-

Test text

-

Test text

- -
- - Test button - -
- - -
- - Test button - -
- - -
- - Test button - -
- -
-''' - -snapshots['test_card_list 1'] = ''' - - - - - - - -''' - -snapshots['test_character_list 1'] = ''' - - -
- -
- Illustration of Xian -
Xian
-

Fun, active, will dance to just about anything that produces a beat. Has great memory, always a joke at hand, might try to introduce memes in Ancient Greece. Scored gold in a track race once and will take any opportunity to bring that up.

-
- -
- Illustration of Jools -
Jools
-

A quick-witted kid who wasn't expecting to embark in a time-warping journey but can't say no to a challenge. Someone has to keep the rest of the group in check, after all!

-
- -
- Illustration of Zayed -
Zayed
-

A pretty chill, curious soul that prefers practice to theory. Always ready to jump into an adventure if it looks interesting enough; not so much otherwise. Probably the one who accidentally turned the time machine on in the first place.

-
- -
-''' - -snapshots['test_character_list[0] 1'] = ''' - - -
- -
- Illustration of Xian -
Xian
-

Fun, active, will dance to just about anything that produces a beat. Has great memory, always a joke at hand, might try to introduce memes in Ancient Greece. Scored gold in a track race once and will take any opportunity to bring that up.

-
- -
- Illustration of Jools -
Jools
-

A quick-witted kid who wasn't expecting to embark in a time-warping journey but can't say no to a challenge. Someone has to keep the rest of the group in check, after all!

-
- -
- Illustration of Zayed -
Zayed
-

A pretty chill, curious soul that prefers practice to theory. Always ready to jump into an adventure if it looks interesting enough; not so much otherwise. Probably the one who accidentally turned the time machine on in the first place.

-
- -
-''' - -snapshots['test_character_list[1] 1'] = ''' - - -
- -
- Illustration of Xian -
Xian
-

Fun, active, will dance to just about anything that produces a beat. Has great memory, always a joke at hand, might try to introduce memes in Ancient Greece. Scored gold in a track race once and will take any opportunity to bring that up.

-
- -
- Illustration of Jools -
Jools
-

A quick-witted kid who wasn't expecting to embark in a time-warping journey but can't say no to a challenge. Someone has to keep the rest of the group in check, after all!

-
- -
- Illustration of Zayed -
Zayed
-

A pretty chill, curious soul that prefers practice to theory. Always ready to jump into an adventure if it looks interesting enough; not so much otherwise. Probably the one who accidentally turned the time machine on in the first place.

-
- -
-''' - -snapshots['test_character_list[2] 1'] = ''' - - -
- -
- Illustration of Xian -
Xian
-

Fun, active, will dance to just about anything that produces a beat. Has great memory, always a joke at hand, might try to introduce memes in Ancient Greece. Scored gold in a track race once and will take any opportunity to bring that up.

-
- -
- Illustration of Jools -
Jools
-

A quick-witted kid who wasn't expecting to embark in a time-warping journey but can't say no to a challenge. Someone has to keep the rest of the group in check, after all!

-
- -
- Illustration of Zayed -
Zayed
-

A pretty chill, curious soul that prefers practice to theory. Always ready to jump into an adventure if it looks interesting enough; not so much otherwise. Probably the one who accidentally turned the time machine on in the first place.

-
- -
-''' - -snapshots['test_character_list[3] 1'] = ''' - - -
- -
- Illustration of Xian -
Xian
-

Fun, active, will dance to just about anything that produces a beat. Has great memory, always a joke at hand, might try to introduce memes in Ancient Greece. Scored gold in a track race once and will take any opportunity to bring that up.

-
- -
- Illustration of Jools -
Jools
-

A quick-witted kid who wasn't expecting to embark in a time-warping journey but can't say no to a challenge. Someone has to keep the rest of the group in check, after all!

-
- -
- Illustration of Zayed -
Zayed
-

A pretty chill, curious soul that prefers practice to theory. Always ready to jump into an adventure if it looks interesting enough; not so much otherwise. Probably the one who accidentally turned the time machine on in the first place.

-
- -
-''' - -snapshots['test_character_list[4] 1'] = ''' - - -
- -
- Illustration of Xian -
Xian
-

Fun, active, will dance to just about anything that produces a beat. Has great memory, always a joke at hand, might try to introduce memes in Ancient Greece. Scored gold in a track race once and will take any opportunity to bring that up.

-
- -
- Illustration of Jools -
Jools
-

A quick-witted kid who wasn't expecting to embark in a time-warping journey but can't say no to a challenge. Someone has to keep the rest of the group in check, after all!

-
- -
- Illustration of Zayed -
Zayed
-

A pretty chill, curious soul that prefers practice to theory. Always ready to jump into an adventure if it looks interesting enough; not so much otherwise. Probably the one who accidentally turned the time machine on in the first place.

-
- -
-''' - -snapshots['test_character_list[5] 1'] = ''' - - -
- -
- Illustration of Xian -
Xian
-

Fun, active, will dance to just about anything that produces a beat. Has great memory, always a joke at hand, might try to introduce memes in Ancient Greece. Scored gold in a track race once and will take any opportunity to bring that up.

-
- -
- Illustration of Jools -
Jools
-

A quick-witted kid who wasn't expecting to embark in a time-warping journey but can't say no to a challenge. Someone has to keep the rest of the group in check, after all!

-
- -
- Illustration of Zayed -
Zayed
-

A pretty chill, curious soul that prefers practice to theory. Always ready to jump into an adventure if it looks interesting enough; not so much otherwise. Probably the one who accidentally turned the time machine on in the first place.

-
- -
-''' - -snapshots['test_character_list[6] 1'] = ''' - - -
- -
- Illustration of Xian -
Xian
-

Fun, active, will dance to just about anything that produces a beat. Has great memory, always a joke at hand, might try to introduce memes in Ancient Greece. Scored gold in a track race once and will take any opportunity to bring that up.

-
- -
- Illustration of Jools -
Jools
-

A quick-witted kid who wasn't expecting to embark in a time-warping journey but can't say no to a challenge. Someone has to keep the rest of the group in check, after all!

-
- -
- Illustration of Zayed -
Zayed
-

A pretty chill, curious soul that prefers practice to theory. Always ready to jump into an adventure if it looks interesting enough; not so much otherwise. Probably the one who accidentally turned the time machine on in the first place.

-
- -
-''' - -snapshots['test_character_list[7] 1'] = ''' - - -
- -
- Illustration of Xian -
Xian
-

Fun, active, will dance to just about anything that produces a beat. Has great memory, always a joke at hand, might try to introduce memes in Ancient Greece. Scored gold in a track race once and will take any opportunity to bring that up.

-
- -
- Illustration of Jools -
Jools
-

A quick-witted kid who wasn't expecting to embark in a time-warping journey but can't say no to a challenge. Someone has to keep the rest of the group in check, after all!

-
- -
- Illustration of Zayed -
Zayed
-

A pretty chill, curious soul that prefers practice to theory. Always ready to jump into an adventure if it looks interesting enough; not so much otherwise. Probably the one who accidentally turned the time machine on in the first place.

-
- -
-''' - -snapshots['test_character_list[8] 1'] = ''' - - -
- -
- Illustration of Xian -
Xian
-

Fun, active, will dance to just about anything that produces a beat. Has great memory, always a joke at hand, might try to introduce memes in Ancient Greece. Scored gold in a track race once and will take any opportunity to bring that up.

-
- -
- Illustration of Jools -
Jools
-

A quick-witted kid who wasn't expecting to embark in a time-warping journey but can't say no to a challenge. Someone has to keep the rest of the group in check, after all!

-
- -
- Illustration of Zayed -
Zayed
-

A pretty chill, curious soul that prefers practice to theory. Always ready to jump into an adventure if it looks interesting enough; not so much otherwise. Probably the one who accidentally turned the time machine on in the first place.

-
- -
-''' - -snapshots['test_character_list[9] 1'] = ''' - - -
- -
- Illustration of Xian -
Xian
-

Fun, active, will dance to just about anything that produces a beat. Has great memory, always a joke at hand, might try to introduce memes in Ancient Greece. Scored gold in a track race once and will take any opportunity to bring that up.

-
- -
- Illustration of Jools -
Jools
-

A quick-witted kid who wasn't expecting to embark in a time-warping journey but can't say no to a challenge. Someone has to keep the rest of the group in check, after all!

-
- -
- Illustration of Zayed -
Zayed
-

A pretty chill, curious soul that prefers practice to theory. Always ready to jump into an adventure if it looks interesting enough; not so much otherwise. Probably the one who accidentally turned the time machine on in the first place.

-
- -
-''' - -snapshots['test_headline 1'] = '''
-

Test title

-
-

Test description

-''' - -snapshots['test_hero_card 1'] = ''' - - -
-
Activated
- -
-

Test title

-

Test description

- -
-
-''' diff --git a/portal/tests/test_aimmo_dashboards.py b/portal/tests/test_aimmo_dashboards.py deleted file mode 100644 index e4ab5e95e..000000000 --- a/portal/tests/test_aimmo_dashboards.py +++ /dev/null @@ -1,206 +0,0 @@ -import json - -import pytest -from aimmo.models import Game -from aimmo.worksheets import WORKSHEETS -from common.models import Class, Teacher -from common.tests.utils.classes import create_class_directly -from common.tests.utils.organisation import create_organisation_directly, join_teacher_to_organisation -from common.tests.utils.student import create_school_student_directly -from common.tests.utils.teacher import signup_teacher_directly -from django.test.client import Client -from django.urls.base import reverse - -from .base_test import BaseTest -from .conftest import IndependentStudent, SchoolStudent - - -# @pytest.mark.django_db -# TODO: move tests to kurono microservice and fix them. -@pytest.mark.skip(reason="Moved game creator to Django") -def test_student_cannot_access_teacher_dashboard(student1: SchoolStudent, class1: Class): - """ - Given you are logged in as a student, - When you try to access the teacher dashboard, - Then you cannot access it and are instead redirected. - """ - c = Client() - url = reverse("student_login", kwargs={"access_code": class1.access_code}) - data = {"username": student1.username, "password": student1.password} - - c.post(url, data) - - student_dashboard_url = reverse("student_aimmo_dashboard") - - response_s = c.get(student_dashboard_url) - - assert response_s.status_code == 200 - - teacher_dashboard_url = reverse("teacher_aimmo_dashboard") - - response = c.get(teacher_dashboard_url) - - assert response.status_code == 302 - - -# @pytest.mark.django_db -# TODO: move tests to kurono microservice and fix them. -@pytest.mark.skip(reason="Moved game creator to Django") -def test_indep_student_cannot_access_dashboard( - independent_student1: IndependentStudent, -): - """ - Given you are logged in as an independent student, - When you try to access the student dashboard, - Then you can access it but the context only has the banner. - """ - c = Client() - url = reverse("independent_student_login") - data = {"username": independent_student1.username, "password": independent_student1.password} - - c.post(url, data) - - student_dashboard_url = reverse("student_aimmo_dashboard") - - response = c.get(student_dashboard_url) - - assert response.status_code == 200 - assert "BANNER" in response.context - assert "HERO_CARD" not in response.context - assert "CARD_LIST" not in response.context - - -# @pytest.mark.django_db -# TODO: move tests to kurono microservice and fix them. -@pytest.mark.skip(reason="Moved game creator to Django") -def test_student_aimmo_dashboard_loads(student1: SchoolStudent, class1: Class, aimmo_game1: Game): - """ - Given an aimmo game is linked to a class, - When a student of that class goes on the Student Kurono Dashboard page, - Then the page loads and the context contains the hero card and card list - associated to the aimmo game. - - Then, given that the class no longer has a game linked to it, - When the student goes on the same page, - Then the page still loads but the context no longer contains the hero card - or the card list elements. - """ - c = Client() - student_login_url = reverse("student_login", kwargs={"access_code": class1.access_code}) - data = {"username": student1.username, "password": student1.password} - - c.post(student_login_url, data) - - student_dashboard_url = reverse("student_aimmo_dashboard") - response = c.get(student_dashboard_url) - - assert response.status_code == 200 - assert "HERO_CARD" in response.context - assert "CARD_LIST" in response.context - - aimmo_game1.delete() - - url = reverse("student_aimmo_dashboard") - response = c.get(url) - - assert response.status_code == 200 - assert "HERO_CARD" not in response.context - assert "CARD_LIST" not in response.context - - -# TODO: move tests to kurono microservice and fix them. -# Selenium tests -# class TestAimmoDashboardFrontend(BaseTest): -# def test_admin_permissions_actions(self): -# # Create admin teacher, school and class -# admin_email, admin_password = signup_teacher_directly() -# school = create_organisation_directly(admin_email) -# admin_class, _, admin_access_code = create_class_directly(admin_email, "class 1") - -# # create another teacher and add as not admin, create a class -# non_admin_email, non_admin_password = signup_teacher_directly() -# join_teacher_to_organisation(non_admin_email, school.name, school.postcode, is_admin=False) -# non_admin_class, _, non_admin_access_code = create_class_directly(non_admin_email, "class 2") - -# non_admin_teacher: Teacher = Teacher.objects.get(new_user__email=non_admin_email) -# admin_teacher: Teacher = Teacher.objects.get(new_user__email=admin_email) - -# c = Client() -# # check if non_admin cannot create a game for the admin -# c.login(username=non_admin_email, password=non_admin_password) -# response = c.post(reverse("teacher_aimmo_dashboard"), {"game_class": admin_class.pk}) -# assert response.status_code == 200 -# assert Game.objects.filter(game_class__teacher__school=school).count() == 0 - -# # create a game by non admin and by admin, then check if admin can delete both -# response = c.post(reverse("teacher_aimmo_dashboard"), {"game_class": non_admin_class.pk}) -# assert response.status_code == 302 -# assert Game.objects.filter(game_class__teacher=non_admin_teacher).count() == 1 -# c.logout() - -# c.login(username=admin_email, password=admin_password) -# response = c.post(reverse("teacher_aimmo_dashboard"), {"game_class": admin_class.pk}) -# assert response.status_code == 302 -# assert Game.objects.filter(game_class__teacher__school=school).count() == 2 - -# admin_game = Game.objects.get(game_class=admin_class) -# non_admin_game = Game.objects.get(game_class=non_admin_class) - -# # test admin deleting games -# c.post(reverse("game-delete-games"), {"game_ids": admin_game.id}) -# c.post(reverse("game-delete-games"), {"game_ids": non_admin_game.id}) -# assert Game.objects.filter(game_class__teacher__school=school, is_archived=True).count() == 2 -# # now make check if the non admin can delete game -# response = c.post(reverse("teacher_aimmo_dashboard"), {"game_class": admin_class.pk}) -# assert response.status_code == 302 -# assert Game.objects.filter(game_class__teacher=admin_teacher, is_archived=False).count() == 1 -# c.logout() - -# c.login(username=non_admin_email, password=non_admin_password) -# response = c.post(reverse("game-delete-games"), {"game_ids": admin_game.id}) -# assert response.status_code == 204 -# assert Game.objects.filter(game_class__teacher=admin_teacher, is_archived=False).count() == 1 - -# def test_worksheet_dropdown_changes_worksheet(self): -# teacher_email, teacher_password = signup_teacher_directly() -# create_organisation_directly(teacher_email) -# klass, class_name, access_code = create_class_directly(teacher_email) -# student_name, student_password, _ = create_school_student_directly(access_code) - -# worksheet1 = WORKSHEETS.get(1) -# worksheet2 = WORKSHEETS.get(2) - -# self.selenium.get(self.live_server_url) -# page = self.go_to_homepage().go_to_teacher_login_page().login(teacher_email, teacher_password) -# page = page.go_to_kurono_teacher_dashboard_page().create_game(klass.id) - -# game = Game.objects.get(game_class=klass) - -# assert game.worksheet == worksheet1 - -# page.change_game_worksheet(worksheet2.id) - -# game = Game.objects.get(game_class=klass) - -# assert game.worksheet == worksheet2 - -# def test_delete_games(self): -# teacher_email, teacher_password = signup_teacher_directly() -# create_organisation_directly(teacher_email) - -# klass1, _, _ = create_class_directly(teacher_email) -# game1 = Game(game_class=klass1) -# game1.save() - -# klass2, _, _ = create_class_directly(teacher_email) -# game2 = Game(game_class=klass2) -# game2.save() - -# assert Game.objects.count() == 2 - -# self.selenium.get(self.live_server_url) -# page = self.go_to_homepage().go_to_teacher_login_page().login(teacher_email, teacher_password) -# page.go_to_kurono_teacher_dashboard_page().delete_games([game1.id, game2.id]) - -# assert Game.objects.filter(is_archived=False).count() == 0 -# assert Game.objects.filter(is_archived=True).count() == 2 diff --git a/portal/tests/test_class.py b/portal/tests/test_class.py index a2e661ebc..7e6ae240f 100644 --- a/portal/tests/test_class.py +++ b/portal/tests/test_class.py @@ -2,10 +2,12 @@ from datetime import datetime, timedelta -from aimmo.models import Game from common.models import Class, DailyActivity, Teacher from common.tests.utils.classes import create_class_directly -from common.tests.utils.organisation import create_organisation_directly, join_teacher_to_organisation +from common.tests.utils.organisation import ( + create_organisation_directly, + join_teacher_to_organisation, +) from common.tests.utils.student import create_school_student_directly from common.tests.utils.teacher import signup_teacher_directly from django.test import Client, TestCase @@ -19,26 +21,6 @@ class TestClass(TestCase): - def test_class_deletion_deletes_game(self): - email, password = signup_teacher_directly() - create_organisation_directly(email) - klass, _, access_code = create_class_directly(email, "class 1") - teacher: Teacher = Teacher.objects.get(new_user__email=email) - c = Client() - c.login(username=email, password=password) - assert Class.objects.filter(teacher=teacher).count() == 1 - - # create a game - response = c.post(reverse("teacher_aimmo_dashboard"), {"game_class": klass.pk}) - assert response.status_code == 302 - assert Game.objects.filter(game_class__teacher=teacher, is_archived=False).count() == 1 - - # try do delete class and see if game is also gone - delete_url = reverse("teacher_delete_class", kwargs={"access_code": access_code}) - c.post(delete_url) - assert Class.objects.filter(teacher=teacher).count() == 0 - assert Game.objects.filter(game_class__teacher=teacher, is_archived=True).count() == 1 - def test_delete_class(self): email1, password1 = signup_teacher_directly() email2, password2 = signup_teacher_directly() @@ -48,7 +30,9 @@ def test_delete_class(self): c = Client() - url = reverse("teacher_delete_class", kwargs={"access_code": access_code}) + url = reverse( + "teacher_delete_class", kwargs={"access_code": access_code} + ) # Login as another teacher, try to delete the class and check for 404 c.login(username=email2, password=password2) @@ -152,19 +136,93 @@ def test_level_control(self): old_daily_activity = DailyActivity(date=old_date) old_daily_activity.save() - url = reverse("teacher_edit_class", kwargs={"access_code": access_code1}) + url = reverse( + "teacher_edit_class", kwargs={"access_code": access_code1} + ) # POST request data for locking only the first level data = { - "Getting Started": ["2", "3", "4", "5", "6", "7", "8", "9", "10", "11", "12"], + "Getting Started": [ + "2", + "3", + "4", + "5", + "6", + "7", + "8", + "9", + "10", + "11", + "12", + ], "Shortest Route": ["13", "14", "15", "16", "17", "18"], - "Loops and Repetitions": ["19", "20", "21", "22", "23", "24", "25", "26", "27", "28"], + "Loops and Repetitions": [ + "19", + "20", + "21", + "22", + "23", + "24", + "25", + "26", + "27", + "28", + ], "Loops with Conditions": ["29", "30", "31", "32"], - "If... Only": ["33", "34", "35", "36", "37", "38", "39", "40", "41", "42", "43"], + "If... Only": [ + "33", + "34", + "35", + "36", + "37", + "38", + "39", + "40", + "41", + "42", + "43", + ], "Traffic Lights": ["44", "45", "46", "47", "48", "49", "50"], - "Limited Blocks": ["53", "78", "79", "80", "81", "82", "83", "84", "54", "55"], + "Limited Blocks": [ + "53", + "78", + "79", + "80", + "81", + "82", + "83", + "84", + "54", + "55", + ], "Procedures": ["85", "52", "60", "86", "62", "87", "61"], - "Blockly Brain Teasers": ["56", "57", "58", "59", "88", "91", "90", "89", "110", "111", "112", "92"], - "Introduction to Python": ["93", "63", "64", "65", "94", "66", "67", "68", "95", "69", "96", "97"], + "Blockly Brain Teasers": [ + "56", + "57", + "58", + "59", + "88", + "91", + "90", + "89", + "110", + "111", + "112", + "92", + ], + "Introduction to Python": [ + "93", + "63", + "64", + "65", + "94", + "66", + "67", + "68", + "95", + "69", + "96", + "97", + ], "Python": [ "98", "70", @@ -203,21 +261,99 @@ def test_level_control(self): assert str(messages[0]) == "Your level preferences have been saved." # test the old analytic stays the same and the new one is incremented - assert DailyActivity.objects.get(date=old_date).level_control_submits == 0 - assert DailyActivity.objects.get(date=datetime.now()).level_control_submits == 1 + assert ( + DailyActivity.objects.get(date=old_date).level_control_submits == 0 + ) + assert ( + DailyActivity.objects.get(date=datetime.now()).level_control_submits + == 1 + ) # Resubmitting to unlock level 1 data = { - "Getting Started": ["1", "2", "3", "4", "5", "6", "7", "8", "9", "10", "11", "12"], + "Getting Started": [ + "1", + "2", + "3", + "4", + "5", + "6", + "7", + "8", + "9", + "10", + "11", + "12", + ], "Shortest Route": ["13", "14", "15", "16", "17", "18"], - "Loops and Repetitions": ["19", "20", "21", "22", "23", "24", "25", "26", "27", "28"], + "Loops and Repetitions": [ + "19", + "20", + "21", + "22", + "23", + "24", + "25", + "26", + "27", + "28", + ], "Loops with Conditions": ["29", "30", "31", "32"], - "If... Only": ["33", "34", "35", "36", "37", "38", "39", "40", "41", "42", "43"], + "If... Only": [ + "33", + "34", + "35", + "36", + "37", + "38", + "39", + "40", + "41", + "42", + "43", + ], "Traffic Lights": ["44", "45", "46", "47", "48", "49", "50"], - "Limited Blocks": ["53", "78", "79", "80", "81", "82", "83", "84", "54", "55"], + "Limited Blocks": [ + "53", + "78", + "79", + "80", + "81", + "82", + "83", + "84", + "54", + "55", + ], "Procedures": ["85", "52", "60", "86", "62", "87", "61"], - "Blockly Brain Teasers": ["56", "57", "58", "59", "88", "91", "90", "89", "110", "111", "112", "92"], - "Introduction to Python": ["93", "63", "64", "65", "94", "66", "67", "68", "95", "69", "96", "97"], + "Blockly Brain Teasers": [ + "56", + "57", + "58", + "59", + "88", + "91", + "90", + "89", + "110", + "111", + "112", + "92", + ], + "Introduction to Python": [ + "93", + "63", + "64", + "65", + "94", + "66", + "67", + "68", + "95", + "69", + "96", + "97", + ], "Python": [ "98", "70", @@ -270,7 +406,9 @@ def test_transfer_class(self): c = Client() - url = reverse("teacher_edit_class", kwargs={"access_code": access_code1}) + url = reverse( + "teacher_edit_class", kwargs={"access_code": access_code1} + ) data = {"new_teacher": teacher2.id, "class_move_submit": ""} # Login as first teacher and transfer class to the second teacher @@ -297,7 +435,12 @@ class TestClassFrontend(BaseTest): def test_create(self): email, password = signup_teacher_directly() create_organisation_directly(email) - page = self.go_to_homepage().go_to_teacher_login_page().login_no_class(email, password).open_classes_tab() + page = ( + self.go_to_homepage() + .go_to_teacher_login_page() + .login_no_class(email, password) + .open_classes_tab() + ) assert page.does_not_have_classes() @@ -312,19 +455,34 @@ def test_create_class_as_admin_for_another_teacher(self): join_teacher_to_organisation(email2, school.name) # Check teacher 2 doesn't have any classes - page = self.go_to_homepage().go_to_teacher_login_page().login(email2, password2).open_classes_tab() + page = ( + self.go_to_homepage() + .go_to_teacher_login_page() + .login(email2, password2) + .open_classes_tab() + ) assert page.does_not_have_classes() page.logout() # Log in as the first teacher and create a class for the second one - page = self.go_to_homepage().go_to_teacher_login_page().login(email1, password1).open_classes_tab() + page = ( + self.go_to_homepage() + .go_to_teacher_login_page() + .login(email1, password1) + .open_classes_tab() + ) page, class_name = create_class(page, teacher_id=teacher2.id) page = TeachClassPage(page.browser) assert is_class_created_message_showing(self.selenium, class_name) page.logout() # Check teacher 2 now has the class - page = self.go_to_homepage().go_to_teacher_login_page().login(email2, password2).open_classes_tab() + page = ( + self.go_to_homepage() + .go_to_teacher_login_page() + .login(email2, password2) + .open_classes_tab() + ) assert page.has_classes() def test_create_dashboard(self): @@ -333,7 +491,12 @@ def test_create_dashboard(self): klass, name, access_code = create_class_directly(email) create_school_student_directly(access_code) - page = self.go_to_homepage().go_to_teacher_login_page().login(email, password).open_classes_tab() + page = ( + self.go_to_homepage() + .go_to_teacher_login_page() + .login(email, password) + .open_classes_tab() + ) page, class_name = create_class(page) @@ -349,7 +512,12 @@ def test_create_dashboard_non_admin(self): klass_2, class_name_2, access_code_2 = create_class_directly(email_2) create_school_student_directly(access_code_2) - page = self.go_to_homepage().go_to_teacher_login_page().login(email_2, password_2).open_classes_tab() + page = ( + self.go_to_homepage() + .go_to_teacher_login_page() + .login(email_2, password_2) + .open_classes_tab() + ) page, class_name_3 = create_class(page) diff --git a/portal/tests/test_independent_student.py b/portal/tests/test_independent_student.py index 0f0c35c79..16092c1f6 100644 --- a/portal/tests/test_independent_student.py +++ b/portal/tests/test_independent_student.py @@ -1,7 +1,6 @@ from __future__ import absolute_import import datetime -import time from unittest.mock import ANY, Mock, patch from common.mail import campaign_ids @@ -28,7 +27,6 @@ from selenium.webdriver.support.wait import WebDriverWait from portal.forms.error_messages import INVALID_LOGIN_MESSAGE - from .base_test import BaseTest from .pageObjects.portal.home_page import HomePage from .utils.messages import ( @@ -658,13 +656,6 @@ def test_join_class_denied_and_accepted_by_admin(self): assert student2.pending_class_request is None assert student2.is_independent() - def test_cannot_see_aimmo(self): - page = self.go_to_homepage() - page, _, username, _, password = create_independent_student(page) - page = page.independent_student_login(username, password) - - assert page.element_does_not_exist_by_link_text("Kurono") - def get_to_forgotten_password_page(self): self.selenium.get(self.live_server_url) page = HomePage(self.selenium).go_to_independent_student_login_page().go_to_indep_forgotten_password_page() diff --git a/portal/tests/test_partials.py b/portal/tests/test_partials.py index 34c370905..07b539bf6 100644 --- a/portal/tests/test_partials.py +++ b/portal/tests/test_partials.py @@ -1,4 +1,3 @@ -import pytest from django.template import Context, Template @@ -12,7 +11,9 @@ def test_banner(snapshot): } context = Context({"BANNER": test_banner}) - template_to_render = Template("{% load banner_tags %}" '{% banner banner_name="BANNER" %}') + template_to_render = Template( + "{% load banner_tags %}" '{% banner banner_name="BANNER" %}' + ) rendered_template = template_to_render.render(context) snapshot.assert_match(rendered_template) @@ -22,7 +23,9 @@ def test_headline(snapshot): test_headline = {"title": "Test title", "description": "Test description"} context = Context({"HEADLINE": test_headline}) - template_to_render = Template("{% load headline_tags %}" '{% headline headline_name="HEADLINE" %}') + template_to_render = Template( + "{% load headline_tags %}" '{% headline headline_name="HEADLINE" %}' + ) rendered_template = template_to_render.render(context) snapshot.assert_match(rendered_template) @@ -57,56 +60,3 @@ def test_benefits(snapshot): rendered_template = template_to_render.render(context) snapshot.assert_match(rendered_template) - - -def test_hero_card(snapshot): - test_hero_card = { - "image": "images/worksheets/future_active.png", - "title": "Test title", - "description": "Test description", - "button1": {"text": "Test button 1", "url": "https://www.codeforlife.education"}, - "button2": {"text": "Test button 2", "url": "kurono/play", "url_args": 1}, - } - - context = Context({"HERO_CARD": test_hero_card}) - - template_to_render = Template("{% load hero_card_tags %}" "{% hero_card hero_card_name='HERO_CARD' %}") - - rendered_template = template_to_render.render(context) - - snapshot.assert_match(rendered_template) - - -def test_card_list(snapshot): - test_card_list = { - "cards": [ - {"image": "images/worksheets/future2.jpg", "title": "Test card 1", "description": "Test description 1"}, - {"image": "images/worksheets/ancient.jpg", "title": "Test card 2", "description": "Test description 2"}, - {"image": "images/worksheets/modern_day.jpg", "title": "Test card 3", "description": "Test description 3"}, - {"image": "images/worksheets/prehistory.jpg", "title": "Test card 4", "description": "Test description 4"}, - { - "image": "images/worksheets/broken_future.jpg", - "title": "Test card 5", - "description": "Test description 5", - }, - ] - } - - context = Context({"CARD_LIST": test_card_list}) - - template_to_render = Template("{% load card_list_tags %}" "{% card_list %}") - - rendered_template = template_to_render.render(context) - - snapshot.assert_match(rendered_template) - - -@pytest.mark.django_db -def test_character_list(snapshot): - context = Context() - - template_to_render = Template("{% load character_list_tags %}" "{% character_list %}") - - rendered_template = template_to_render.render(context) - - snapshot.assert_match(rendered_template) diff --git a/portal/tests/test_teacher.py b/portal/tests/test_teacher.py index 86af2c575..148d7ad30 100644 --- a/portal/tests/test_teacher.py +++ b/portal/tests/test_teacher.py @@ -6,9 +6,7 @@ from uuid import uuid4 import jwt -from aimmo.models import Game from common.mail import campaign_ids -from common.models import Class, Student, Teacher from common.tests.utils import email as email_utils from common.tests.utils.classes import create_class_directly from common.tests.utils.organisation import ( @@ -50,229 +48,6 @@ class TestTeacher(TestCase): - def test_new_student_can_play_games(self): - """ - Given a teacher has a kurono game, - When they add a new student to their class, - Then the new student should be able to play that class's games - """ - email, password = signup_teacher_directly() - create_organisation_directly(email) - klass, _, access_code = create_class_directly(email) - create_school_student_directly(access_code) - - c = Client() - c.login(username=email, password=password) - c.post(reverse("teacher_aimmo_dashboard"), {"game_class": klass.id}) - c.post(reverse("view_class", kwargs={"access_code": access_code}), {"names": "Florian"}) - - game = Game.objects.get(id=1) - new_student = Student.objects.last() - assert game.can_user_play(new_student.new_user) - - def test_accepted_independent_student_can_play_games(self): - """ - Given an independent student requests access to a class, - When the teacher for that class accepts the request, - Then the new student should have access to that class's games - """ - email, password = signup_teacher_directly() - create_organisation_directly(email) - klass, _, access_code = create_class_directly(email) - klass.always_accept_requests = True - klass.save() - create_school_student_directly(access_code) - indep_username, indep_password, indep_student = create_independent_student_directly() - - c = Client() - - c.login(username=indep_username, password=indep_password) - c.post(reverse("student_join_organisation"), {"access_code": access_code, "class_join_request": "Request"}) - c.logout() - - c.login(username=email, password=password) - c.post(reverse("teacher_aimmo_dashboard"), {"game_class": klass.pk}) - c.post(reverse("teacher_accept_student_request", kwargs={"pk": indep_student.pk}), {"name": "Florian"}) - - game: Game = Game.objects.get(id=1) - new_student = Student.objects.last() - assert game.can_user_play(new_student.new_user) - - def test_moved_class_has_correct_permissions_for_students_and_teachers(self): - """ - Given two teachers each with a class and an aimmo game, - When teacher 1 transfers their class to teacher 2, - Then: - - Students in each class still only have access to their class games - - Teacher 2 has access to both games and teacher 1 has access to none - """ - - # Create teacher 1 -> class 1 -> student 1 - email1, password1 = signup_teacher_directly() - school = create_organisation_directly(email1) - klass1, _, access_code1 = create_class_directly(email1, "Class 1") - create_school_student_directly(access_code1) - - # Create teacher 2 -> class 2 -> student 2 - email2, password2 = signup_teacher_directly() - join_teacher_to_organisation(email2, school.name) - klass2, _, access_code2 = create_class_directly(email2, "Class 2") - create_school_student_directly(access_code2) - - teacher2: Teacher = Teacher.objects.get(new_user__email=email2) - # make teacher1 non admin to remove extra permissions - teacher1: Teacher = Teacher.objects.get(new_user__email=email1) - teacher1.is_admin = False - teacher1.save() - - c = Client() - - # Create game 1 under class 1 - c.login(username=email1, password=password1) - c.post(reverse("teacher_aimmo_dashboard"), {"game_class": klass1.pk}) - c.logout() - - # Create game 2 under class 2 - c.login(username=email2, password=password2) - c.post(reverse("teacher_aimmo_dashboard"), {"game_class": klass2.pk}) - c.logout() - - game1: Game = Game.objects.get(owner=teacher1.new_user) - game2: Game = Game.objects.get(owner=teacher2.new_user) - - student1: Student = Student.objects.get(class_field=klass1) - student2: Student = Student.objects.get(class_field=klass2) - - # Check student permissions for each game - assert game1.can_user_play(student1.new_user) - assert game2.can_user_play(student2.new_user) - assert not game1.can_user_play(student2.new_user) - assert not game2.can_user_play(student1.new_user) - - # Check teacher permissions for each game - assert game1.can_user_play(teacher1.new_user) - assert game2.can_user_play(teacher2.new_user) - assert not game1.can_user_play(teacher2.new_user) - assert not game2.can_user_play(teacher1.new_user) - - # Transfer class 1 over to teacher 2 - c.login(username=email1, password=password1) - response = c.post( - reverse("teacher_edit_class", kwargs={"access_code": access_code1}), - {"new_teacher": teacher2.pk, "class_move_submit": ""}, - ) - assert response.status_code == 302 - c.logout() - - # Refresh model instances - klass1: Class = Class.objects.get(pk=klass1.pk) - game1 = Game.objects.get(pk=game1.pk) - game2 = Game.objects.get(pk=game2.pk) - - # Check teacher 2 is the teacher for class 1 - assert klass1.teacher == teacher2 - - # Check that the students' permissions have not changed - assert game1.can_user_play(student1.new_user) - assert game2.can_user_play(student2.new_user) - assert not game1.can_user_play(student2.new_user) - assert not game2.can_user_play(student1.new_user) - - # Check that teacher 1 cannot access class 1's game 1 anymore - assert not game1.can_user_play(teacher1.new_user) - - # Check that teacher 2 can access game 1 - assert game1.can_user_play(teacher2.new_user) - - def test_moved_student_has_access_to_only_new_teacher_games(self): - """ - Given a student in a class, - When a teacher transfers them to another class with a new teacher, - Then the student should only have access to the new teacher's games - """ - - email1, password1 = signup_teacher_directly() - school = create_organisation_directly(email1) - klass1, _, access_code1 = create_class_directly(email1, "Class 1") - create_school_student_directly(access_code1) - - email2, password2 = signup_teacher_directly() - join_teacher_to_organisation(email2, school.name) - klass2, _, access_code2 = create_class_directly(email2, "Class 2") - create_school_student_directly(access_code2) - - teacher1 = Teacher.objects.get(new_user__email=email1) - teacher2 = Teacher.objects.get(new_user__email=email2) - - c = Client() - c.login(username=email2, password=password2) - c.post(reverse("teacher_aimmo_dashboard"), {"game_class": klass2.pk}) - c.logout() - - c.login(username=email1, password=password1) - c.post(reverse("teacher_aimmo_dashboard"), {"game_class": klass1.pk}) - - game1 = Game.objects.get(owner=teacher1.new_user) - game2 = Game.objects.get(owner=teacher2.new_user) - - student1 = Student.objects.get(class_field=klass1) - student2 = Student.objects.get(class_field=klass2) - - assert game1.can_user_play(student1.new_user) - assert game2.can_user_play(student2.new_user) - - c.post( - reverse("teacher_move_students", kwargs={"access_code": access_code1}), {"transfer_students": student1.pk} - ) - c.post( - reverse("teacher_move_students_to_class", kwargs={"access_code": access_code1}), - { - "form-0-name": student1.user.user.first_name, - "form-MAX_NUM_FORMS": 1000, - "form-0-orig_name": student1.user.user.first_name, - "form-TOTAL_FORMS": 1, - "form-MIN_NUM_FORMS": 0, - "submit_disambiguation": "", - "form-INITIAL_FORMS": 1, - "new_class": klass2.pk, - }, - ) - c.logout() - - game1 = Game.objects.get(owner=teacher1.new_user) - game2 = Game.objects.get(owner=teacher2.new_user) - - assert not game1.can_user_play(student1.new_user) - assert game2.can_user_play(student1.new_user) - - def test_teacher_cannot_create_duplicate_game(self): - """ - Given a teacher, a class and a worksheet, - When the teacher creates a game for that class and worksheet, and then tries to - create the exact same game again, - Then the class should only have one game, and an error message should appear. - """ - - email, password = signup_teacher_directly() - create_organisation_directly(email) - klass, _, _ = create_class_directly(email) - - c = Client() - c.login(username=email, password=password) - game1_response = c.post(reverse("teacher_aimmo_dashboard"), {"game_class": klass.pk}) - - assert game1_response.status_code == 302 - assert Game.objects.filter(game_class=klass, is_archived=False).count() == 1 - assert klass.active_game is not None - messages = list(game1_response.wsgi_request._messages) - assert len([m for m in messages if m.tags == "warning"]) == 0 - - game2_response = c.post(reverse("teacher_aimmo_dashboard"), {"game_class": klass.pk}) - - messages = list(game2_response.wsgi_request._messages) - assert len([m for m in messages if m.tags == "warning"]) == 1 - assert messages[0].message == "An active game already exists for this class" - def test_signup_short_password_fails(self): c = Client() @@ -404,7 +179,9 @@ def test_signup_email_verification(self, mock_send_dotdigital_email: Mock): assert bad_verification_response.status_code == 200 # Get verification link from function call - verification_url = mock_send_dotdigital_email.call_args.kwargs["personalization_values"]["VERIFICATION_LINK"] + verification_url = mock_send_dotdigital_email.call_args.kwargs[ + "personalization_values" + ]["VERIFICATION_LINK"] # Verify the email properly verification_response = c.get(verification_url) @@ -424,7 +201,14 @@ class TestTeacherFrontend(BaseTest): def test_password_too_common(self): self.selenium.get(self.live_server_url) page = HomePage(self.selenium).go_to_signup_page() - page = page.signup("first_name", "last_name", "e@ma.il", "Password123$", "Password123$", success=False) + page = page.signup( + "first_name", + "last_name", + "e@ma.il", + "Password123$", + "Password123$", + success=False, + ) try: submit_button = WebDriverWait(self.selenium, 10).until( EC.element_to_be_clickable((By.NAME, "teacher_signup")) @@ -432,7 +216,8 @@ def test_password_too_common(self): submit_button.click() except: assert page.was_form_invalid( - "form-reg-teacher", "Password is too common, consider using a different password." + "form-reg-teacher", + "Password is too common, consider using a different password.", ) def test_signup_without_newsletter(self): @@ -467,8 +252,12 @@ def test_login_failure(self): self.selenium.get(self.live_server_url) page = HomePage(self.selenium) page = page.go_to_teacher_login_page() - page = page.login_failure("non-existent-email@codeforlife.com", "Incorrect password") - assert page.has_login_failed("form-login-teacher", INVALID_LOGIN_MESSAGE) + page = page.login_failure( + "non-existent-email@codeforlife.com", "Incorrect password" + ) + assert page.has_login_failed( + "form-login-teacher", INVALID_LOGIN_MESSAGE + ) def test_login_success(self): email, password = signup_teacher_directly() @@ -492,9 +281,13 @@ def test_login_not_verified(self, mock_send_dotdigital_email): page = page.go_to_teacher_login_page() page = page.login_failure(email, password) - assert page.has_login_failed("form-login-teacher", INVALID_LOGIN_MESSAGE) + assert page.has_login_failed( + "form-login-teacher", INVALID_LOGIN_MESSAGE + ) - verification_url = mock_send_dotdigital_email.call_args.kwargs["personalization_values"]["VERIFICATION_LINK"] + verification_url = mock_send_dotdigital_email.call_args.kwargs[ + "personalization_values" + ]["VERIFICATION_LINK"] verify_email(page, verification_url) @@ -518,15 +311,26 @@ def test_edit_details(self): create_school_student_directly(access_code) self.selenium.get(self.live_server_url) - page = HomePage(self.selenium).go_to_teacher_login_page().login(email, password).open_account_tab() + page = ( + HomePage(self.selenium) + .go_to_teacher_login_page() + .login(email, password) + .open_account_tab() + ) page = page.change_teacher_details( - {"first_name": "Paulina", "last_name": "Koch", "current_password": "$RFVBGT%6yhn"} + { + "first_name": "Paulina", + "last_name": "Koch", + "current_password": "$RFVBGT%6yhn", + } ) assert self.is_dashboard_page(page) assert is_teacher_details_updated_message_showing(self.selenium) - assert page.check_account_details({"first_name": "Paulina", "last_name": "Koch"}) + assert page.check_account_details( + {"first_name": "Paulina", "last_name": "Koch"} + ) def test_edit_details_non_admin(self): email_1, _ = signup_teacher_directly() @@ -539,15 +343,26 @@ def test_edit_details_non_admin(self): create_school_student_directly(access_code_2) self.selenium.get(self.live_server_url) - page = HomePage(self.selenium).go_to_teacher_login_page().login(email_2, password_2).open_account_tab() + page = ( + HomePage(self.selenium) + .go_to_teacher_login_page() + .login(email_2, password_2) + .open_account_tab() + ) page = page.change_teacher_details( - {"first_name": "Florian", "last_name": "Aucomte", "current_password": password_2} + { + "first_name": "Florian", + "last_name": "Aucomte", + "current_password": password_2, + } ) assert self.is_dashboard_page(page) assert is_teacher_details_updated_message_showing(self.selenium) - assert page.check_account_details({"first_name": "Florian", "last_name": "Aucomte"}) + assert page.check_account_details( + {"first_name": "Florian", "last_name": "Aucomte"} + ) @patch("common.helpers.emails.send_dotdigital_email") def test_change_email(self, mock_send_dotdigital_email): @@ -559,7 +374,11 @@ def test_change_email(self, mock_send_dotdigital_email): other_email, _ = signup_teacher_directly() page = self.go_to_homepage() - page = page.go_to_teacher_login_page().login(email, password).open_account_tab() + page = ( + page.go_to_teacher_login_page() + .login(email, password) + .open_account_tab() + ) # Try changing email to an existing email, should fail page = page.change_email("Test", "Teacher", other_email, password) @@ -567,24 +386,36 @@ def test_change_email(self, mock_send_dotdigital_email): assert is_email_updated_message_showing(self.selenium) mock_send_dotdigital_email.assert_called_with( - campaign_ids["email_change_notification"], ANY, personalization_values=ANY + campaign_ids["email_change_notification"], + ANY, + personalization_values=ANY, ) # Try changing email to an existing indy student's email, should fail indy_email, _, _ = create_independent_student_directly() page = self.go_to_homepage() - page = page.go_to_teacher_login_page().login(email, password).open_account_tab() + page = ( + page.go_to_teacher_login_page() + .login(email, password) + .open_account_tab() + ) page = page.change_email("Test", "Teacher", indy_email, password) assert self.is_email_verification_page(page) assert is_email_updated_message_showing(self.selenium) mock_send_dotdigital_email.assert_called_with( - campaign_ids["email_change_notification"], ANY, personalization_values=ANY + campaign_ids["email_change_notification"], + ANY, + personalization_values=ANY, ) page = self.go_to_homepage() - page = page.go_to_teacher_login_page().login(email, password).open_account_tab() + page = ( + page.go_to_teacher_login_page() + .login(email, password) + .open_account_tab() + ) # Try changing email to a new one, should succeed new_email = "another-email@codeforlife.com" @@ -594,21 +425,33 @@ def test_change_email(self, mock_send_dotdigital_email): # Check user can still log in with old account before verifying new email self.selenium.get(self.live_server_url) - page = HomePage(self.selenium).go_to_teacher_login_page().login(email, password) + page = ( + HomePage(self.selenium) + .go_to_teacher_login_page() + .login(email, password) + ) assert self.is_dashboard_page(page) page = page.logout() mock_send_dotdigital_email.assert_called_with( - campaign_ids["email_change_verification"], ANY, personalization_values=ANY + campaign_ids["email_change_verification"], + ANY, + personalization_values=ANY, ) - verification_url = mock_send_dotdigital_email.call_args.kwargs["personalization_values"]["VERIFICATION_LINK"] + verification_url = mock_send_dotdigital_email.call_args.kwargs[ + "personalization_values" + ]["VERIFICATION_LINK"] - page = email_utils.follow_change_email_link_to_dashboard(page, verification_url) + page = email_utils.follow_change_email_link_to_dashboard( + page, verification_url + ) page = page.login(new_email, password).open_account_tab() - assert page.check_account_details({"first_name": "Test", "last_name": "Teacher"}) + assert page.check_account_details( + {"first_name": "Test", "last_name": "Teacher"} + ) def test_change_password(self): email, password = signup_teacher_directly() @@ -617,7 +460,12 @@ def test_change_password(self): create_school_student_directly(access_code) self.selenium.get(self.live_server_url) - page = HomePage(self.selenium).go_to_teacher_login_page().login(email, password).open_account_tab() + page = ( + HomePage(self.selenium) + .go_to_teacher_login_page() + .login(email, password) + .open_account_tab() + ) new_password = "AnotherPassword12!" page = page.change_password("Test", "Teacher", new_password, password) @@ -639,20 +487,28 @@ def test_reset_password(self, mock_send_dotdigital_email: Mock): page.reset_email_submit(email) - mock_send_dotdigital_email.assert_called_with(campaign_ids["reset_password"], ANY, personalization_values=ANY) + mock_send_dotdigital_email.assert_called_with( + campaign_ids["reset_password"], ANY, personalization_values=ANY + ) - reset_password_url = mock_send_dotdigital_email.call_args.kwargs["personalization_values"][ - "RESET_PASSWORD_LINK" - ] + reset_password_url = mock_send_dotdigital_email.call_args.kwargs[ + "personalization_values" + ]["RESET_PASSWORD_LINK"] - page = email_utils.follow_reset_email_link(self.selenium, reset_password_url) + page = email_utils.follow_reset_email_link( + self.selenium, reset_password_url + ) new_password = "AnotherPassword12!" page.teacher_reset_password(new_password) self.selenium.get(self.live_server_url) - page = HomePage(self.selenium).go_to_teacher_login_page().login(email, new_password) + page = ( + HomePage(self.selenium) + .go_to_teacher_login_page() + .login(email, new_password) + ) assert self.is_dashboard_page(page) @patch("portal.forms.registration.send_dotdigital_email") @@ -666,18 +522,25 @@ def test_reset_with_same_password(self, mock_send_dotdigital_email: Mock): page.reset_email_submit(email) - mock_send_dotdigital_email.assert_called_with(campaign_ids["reset_password"], ANY, personalization_values=ANY) + mock_send_dotdigital_email.assert_called_with( + campaign_ids["reset_password"], ANY, personalization_values=ANY + ) - reset_password_url = mock_send_dotdigital_email.call_args.kwargs["personalization_values"][ - "RESET_PASSWORD_LINK" - ] + reset_password_url = mock_send_dotdigital_email.call_args.kwargs[ + "personalization_values" + ]["RESET_PASSWORD_LINK"] - page = email_utils.follow_reset_email_link(self.selenium, reset_password_url) + page = email_utils.follow_reset_email_link( + self.selenium, reset_password_url + ) page.reset_password_fail(password) message = page.browser.find_element(By.CLASS_NAME, "errorlist") - assert "Please choose a password that you haven't used before" in message.text + assert ( + "Please choose a password that you haven't used before" + in message.text + ) @patch("portal.forms.registration.send_dotdigital_email") def test_reset_password_fail(self, mock_send_dotdigital_email: Mock): @@ -698,7 +561,10 @@ def test_admin_sees_all_school_classes(self): join_teacher_to_organisation(standard_email, school.name) page = ( - self.go_to_homepage().go_to_teacher_login_page().login(standard_email, standard_password).open_classes_tab() + self.go_to_homepage() + .go_to_teacher_login_page() + .login(standard_email, standard_password) + .open_classes_tab() ) assert page.element_does_not_exist_by_id(f"class-code-{access_code}") @@ -710,8 +576,15 @@ def test_admin_sees_all_school_classes(self): admin_email, admin_password = signup_teacher_directly() join_teacher_to_organisation(admin_email, school.name, is_admin=True) - page = self.go_to_homepage().go_to_teacher_login_page().login(admin_email, admin_password).open_classes_tab() - class_code_field = page.browser.find_element(By.ID, f"class-code-{access_code}") + page = ( + self.go_to_homepage() + .go_to_teacher_login_page() + .login(admin_email, admin_password) + .open_classes_tab() + ) + class_code_field = page.browser.find_element( + By.ID, f"class-code-{access_code}" + ) assert class_code_field.text == access_code def test_admin_student_edit(self): @@ -719,13 +592,20 @@ def test_admin_student_edit(self): school = create_organisation_directly(email) klass, _, access_code = create_class_directly(email, "class123") - student_name, student_password, student_student = create_school_student_directly(access_code) + ( + student_name, + student_password, + student_student, + ) = create_school_student_directly(access_code) joining_email, joining_password = signup_teacher_directly() join_teacher_to_organisation(joining_email, school.name, is_admin=True) page = ( - self.go_to_homepage().go_to_teacher_login_page().login(joining_email, joining_password).open_classes_tab() + self.go_to_homepage() + .go_to_teacher_login_page() + .login(joining_email, joining_password) + .open_classes_tab() ) class_button = WebDriverWait(self.selenium, WAIT_TIME).until( @@ -739,21 +619,34 @@ def test_admin_student_edit(self): edit_student_button.click() title = page.browser.find_element(By.ID, "student_details") - assert title.text == f"Edit student details for {student_name} from class {klass} ({access_code})" + assert ( + title.text + == f"Edit student details for {student_name} from class {klass} ({access_code})" + ) def test_make_admin_popup(self): email, password = signup_teacher_directly() school = create_organisation_directly(email) - page = self.go_to_homepage().go_to_teacher_login_page().login(email, password) + page = ( + self.go_to_homepage() + .go_to_teacher_login_page() + .login(email, password) + ) joining_email, _ = signup_teacher_directly() - invite_data = {"teacher_first_name": "Real", "teacher_last_name": "Name", "teacher_email": "ren@me.me"} + invite_data = { + "teacher_first_name": "Real", + "teacher_last_name": "Name", + "teacher_email": "ren@me.me", + } for key in invite_data.keys(): field = page.browser.find_element(By.NAME, key) field.send_keys(invite_data[key]) - invite_button = page.browser.find_element(By.NAME, "invite_teacher_button") + invite_button = page.browser.find_element( + By.NAME, "invite_teacher_button" + ) invite_button.click() # Once invite sent test the make admin button @@ -764,7 +657,11 @@ def test_make_admin_popup(self): make_admin_button.click() """ - button_ids = ["make_admin_button_invite", "cancel_admin_popup_button", "delete-invite"] + button_ids = [ + "make_admin_button_invite", + "cancel_admin_popup_button", + "delete-invite", + ] click_buttons_by_id(page, self, button_ids) # Delete the invite and check if the form invite with @@ -797,27 +694,40 @@ def test_delete_account(self): create_organisation_directly(email) self.selenium.get(self.live_server_url) - page = HomePage(self.selenium).go_to_teacher_login_page().login(email, password).open_account_tab() + page = ( + HomePage(self.selenium) + .go_to_teacher_login_page() + .login(email, password) + .open_account_tab() + ) # test incorrect password - page.browser.find_element(By.ID, "id_delete_password").send_keys("IncorrectPassword") + page.browser.find_element(By.ID, "id_delete_password").send_keys( + "IncorrectPassword" + ) page.browser.find_element(By.ID, "delete_account_button").click() is_message_showing(page.browser, "Your account was not deleted") # test cancel (no class) time.sleep(FADE_TIME) page.browser.find_element(By.ID, "id_delete_password").clear() - page.browser.find_element(By.ID, "id_delete_password").send_keys(password) + page.browser.find_element(By.ID, "id_delete_password").send_keys( + password + ) page.browser.find_element(By.ID, "delete_account_button").click() time.sleep(FADE_TIME) - assert page.browser.find_element(By.ID, "popup-delete-review").is_displayed() + assert page.browser.find_element( + By.ID, "popup-delete-review" + ).is_displayed() page.browser.find_element(By.ID, "cancel_popup_button").click() time.sleep(FADE_TIME) # test close button in the corner page.browser.find_element(By.ID, "id_delete_password").clear() - page.browser.find_element(By.ID, "id_delete_password").send_keys(password) + page.browser.find_element(By.ID, "id_delete_password").send_keys( + password + ) page.browser.find_element(By.ID, "delete_account_button").click() time.sleep(FADE_TIME) @@ -829,11 +739,15 @@ def test_delete_account(self): create_school_student_directly(access_code) # delete then review classes - page.browser.find_element(By.ID, "id_delete_password").send_keys(password) + page.browser.find_element(By.ID, "id_delete_password").send_keys( + password + ) page.browser.find_element(By.ID, "delete_account_button").click() time.sleep(FADE_TIME) - assert page.browser.find_element(By.ID, "popup-delete-review").is_displayed() + assert page.browser.find_element( + By.ID, "popup-delete-review" + ).is_displayed() page.browser.find_element(By.ID, "review_button").click() time.sleep(FADE_TIME) @@ -841,7 +755,9 @@ def test_delete_account(self): page = page.open_account_tab() # test actual deletion - page.browser.find_element(By.ID, "id_delete_password").send_keys(password) + page.browser.find_element(By.ID, "id_delete_password").send_keys( + password + ) page.browser.find_element(By.ID, "delete_account_button").click() time.sleep(FADE_TIME) @@ -851,29 +767,49 @@ def test_delete_account(self): assert page.browser.find_element(By.CLASS_NAME, "banner--homepage") # user should not be able to login now - page = HomePage(self.selenium).go_to_teacher_login_page().login_failure(email, password) + page = ( + HomePage(self.selenium) + .go_to_teacher_login_page() + .login_failure(email, password) + ) - assert page.has_login_failed("form-login-teacher", INVALID_LOGIN_MESSAGE) + assert page.has_login_failed( + "form-login-teacher", INVALID_LOGIN_MESSAGE + ) def test_onboarding_complete(self): email, password = signup_teacher_directly() self.selenium.get(self.live_server_url) - page = HomePage(self.selenium).go_to_teacher_login_page().login_no_school(email, password) + page = ( + HomePage(self.selenium) + .go_to_teacher_login_page() + .login_no_school(email, password) + ) page = page.create_organisation("Test school", "W1", "GB") page = page.create_class("Test class", True) - page = page.type_student_name("Test Student").create_students().complete_setup() + page = ( + page.type_student_name("Test Student") + .create_students() + .complete_setup() + ) assert page.has_onboarding_complete_popup() def get_to_forgotten_password_page(self): self.selenium.get(self.live_server_url) - page = HomePage(self.selenium).go_to_teacher_login_page().go_to_teacher_forgotten_password_page() + page = ( + HomePage(self.selenium) + .go_to_teacher_login_page() + .go_to_teacher_forgotten_password_page() + ) return page def wait_for_email(self): - WebDriverWait(self.selenium, 2).until(lambda driver: len(mail.outbox) == 1) + WebDriverWait(self.selenium, 2).until( + lambda driver: len(mail.outbox) == 1 + ) def is_dashboard_page(self, page): return page.__class__.__name__ == "TeachDashboardPage" diff --git a/portal/tests/test_views.py b/portal/tests/test_views.py index e87fcbb34..dfaaa711b 100644 --- a/portal/tests/test_views.py +++ b/portal/tests/test_views.py @@ -6,7 +6,6 @@ import PyPDF2 import pytest -from aimmo.models import Game from common.models import ( Class, DailyActivity, @@ -55,7 +54,9 @@ def setUpTestData(cls): cls.email, cls.password = signup_teacher_directly() cls.school = create_organisation_directly(cls.email) _, _, cls.class_access_code = create_class_directly(cls.email) - _, cls.password_student, cls.student = create_school_student_directly(cls.class_access_code) + _, cls.password_student, cls.student = create_school_student_directly( + cls.class_access_code + ) def login(self): c = Client() @@ -64,7 +65,9 @@ def login(self): def test_reminder_cards(self): c = self.login() - url = reverse("teacher_print_reminder_cards", args=[self.class_access_code]) + url = reverse( + "teacher_print_reminder_cards", args=[self.class_access_code] + ) # First test with 2 dummy students NAME1 = "Test name" @@ -98,7 +101,9 @@ def test_reminder_cards(self): # page number students_per_page = REMINDER_CARDS_PDF_ROWS * REMINDER_CARDS_PDF_COLUMNS for _ in range(len(studentlist), students_per_page + 1): - studentlist.append({"name": NAME1, "password": PASSWORD1, "login_url": URL}) + studentlist.append( + {"name": NAME1, "password": PASSWORD1, "login_url": URL} + ) assert len(studentlist) == students_per_page + 1 @@ -137,7 +142,9 @@ def test_csv(self): reader = csv.reader(io.StringIO(content)) access_code = self.class_access_code - class_url = reverse("student_login", kwargs={"access_code": access_code}) + class_url = reverse( + "student_login", kwargs={"access_code": access_code} + ) row0 = next(reader) assert row0[0].strip() == access_code assert class_url in row0[1].strip() @@ -176,7 +183,9 @@ def test_organisation_kick_has_correct_permissions(self): def test_daily_activity_student_details(self): c = self.login() - url = reverse("teacher_print_reminder_cards", args=[self.class_access_code]) + url = reverse( + "teacher_print_reminder_cards", args=[self.class_access_code] + ) data = { "data": json.dumps( @@ -224,7 +233,9 @@ def test_daily_activity_student_details(self): def test_release_verified_student(self): c = Client() - student_login_url = reverse("student_login", args=[self.class_access_code]) + student_login_url = reverse( + "student_login", args=[self.class_access_code] + ) response = c.post( student_login_url, { @@ -240,7 +251,9 @@ def test_release_verified_student(self): c.logout() c.login(username=self.email, password=self.password) - release_url = reverse("teacher_dismiss_students", args=[self.class_access_code]) + release_url = reverse( + "teacher_dismiss_students", args=[self.class_access_code] + ) response = c.post( release_url, { @@ -277,7 +290,9 @@ def _set_up_test_data(self): teacher_email, teacher_password = signup_teacher_directly() create_organisation_directly(teacher_email) _, _, class_access_code = create_class_directly(teacher_email) - student_name, student_password, _ = create_school_student_directly(class_access_code) + student_name, student_password, _ = create_school_student_directly( + class_access_code + ) return ( teacher_email, @@ -310,9 +325,16 @@ def _create_and_login_school_student(self, next_url=False): _, _, name, password, class_access_code = self._set_up_test_data() if next_url: - url = reverse("student_login", kwargs={"access_code": class_access_code}) + "?next=/" + url = ( + reverse( + "student_login", kwargs={"access_code": class_access_code} + ) + + "?next=/" + ) else: - url = reverse("student_login", kwargs={"access_code": class_access_code}) + url = reverse( + "student_login", kwargs={"access_code": class_access_code} + ) c = Client() response = c.post(url, {"username": name, "password": password}) @@ -351,7 +373,9 @@ def test_teacher_session(self): def _get_user_class(self, name, class_access_code): klass = Class.objects.get(access_code=class_access_code) - students = Student.objects.filter(new_user__first_name__iexact=name, class_field=klass) + students = Student.objects.filter( + new_user__first_name__iexact=name, class_field=klass + ) assert len(students) == 1 user = students[0].new_user return user, klass @@ -393,7 +417,9 @@ def test_student_session_class_link(self): _, _, name, password, class_access_code = self._set_up_test_data() c = Client() - url = reverse("student_login", kwargs={"access_code": class_access_code}) + url = reverse( + "student_login", kwargs={"access_code": class_access_code} + ) c.post(url, {"username": name, "password": password}) # check if there's a UserSession data within the last 10 secs @@ -414,7 +440,9 @@ def test_student_login_failed(self): randomname = "randomname" c = Client() - url = reverse("student_login", kwargs={"access_code": class_access_code}) + url = reverse( + "student_login", kwargs={"access_code": class_access_code} + ) c.post(url, {"username": randomname, "password": "xx"}) # check if there's a UserSession data within the last 10 secs @@ -440,7 +468,9 @@ def test_indep_student_session(self): def test_student_direct_login(self): _, _, _, _, class_access_code = self._set_up_test_data() - student, login_id, _, _ = create_student_with_direct_login(class_access_code) + student, login_id, _, _ = create_student_with_direct_login( + class_access_code + ) c = Client() assert c.login(user_id=student.new_user.id, login_id=login_id) == True @@ -543,7 +573,8 @@ def test_student_dashboard_view(self): "total_available_score": 2040, } - # Expected context data when a student has also attempted some custom RR levels + # Expected context data when a student has also attempted some custom RR + # levels EXPECTED_DATA_WITH_CUSTOM_ATTEMPTS = { "num_completed": 2, "num_top_scores": 1, @@ -553,22 +584,12 @@ def test_student_dashboard_view(self): "total_custom_available_score": 20, } - # Expected context data when a student also has access to a Kurono game - EXPECTED_DATA_WITH_KURONO_GAME = { - "num_completed": 2, - "num_top_scores": 1, - "total_score": 39, - "total_available_score": 2040, - "total_custom_score": 10, - "total_custom_available_score": 20, - "worksheet_id": 3, - "worksheet_image": "images/worksheets/ancient.jpg", - } - c = Client() # Login and check initial data - url = reverse("student_login", kwargs={"access_code": class_access_code}) + url = reverse( + "student_login", kwargs={"access_code": class_access_code} + ) c.post(url, {"username": student_name, "password": student_password}) student_dashboard_url = reverse("student_details") @@ -589,9 +610,9 @@ def test_student_dashboard_view(self): assert response.status_code == 200 assert response.context_data == EXPECTED_DATA_WITH_ATTEMPTS - # Teacher creates 3 custom levels, only shares the first 2 with the student. - # Check that the total available score only includes the levels shared with the - # student. Student attempts one level only. + # Teacher creates 3 custom levels, only shares the first 2 with the + # student. Check that the total available score only includes the + # levels shared with the student. Student attempts one level only. custom_level1_id = create_save_level(student.class_field.teacher) custom_level2_id = create_save_level(student.class_field.teacher) create_save_level(student.class_field.teacher) @@ -608,15 +629,6 @@ def test_student_dashboard_view(self): assert response.status_code == 200 assert response.context_data == EXPECTED_DATA_WITH_CUSTOM_ATTEMPTS - # Link Kurono game to student's class - game = Game(game_class=klass, worksheet_id=3) - game.save() - - response = c.get(student_dashboard_url) - - assert response.status_code == 200 - assert response.context_data == EXPECTED_DATA_WITH_KURONO_GAME - @patch("portal.views.registration.send_dotdigital_email") def test_delete_account(self, mock_send_dotdigital_email: Mock): email, password = signup_teacher_directly() @@ -647,7 +659,9 @@ def test_delete_account(self, mock_send_dotdigital_email: Mock): # try again with the correct password url = reverse("delete_account") - response = c.post(url, {"password": password, "unsubscribe_newsletter": "on"}) + response = c.post( + url, {"password": password, "unsubscribe_newsletter": "on"} + ) assert response.status_code == 302 mock_send_dotdigital_email.assert_called_once() @@ -729,7 +743,9 @@ def test_delete_account_admin(self, mock_send_dotdigital_email: Mock): school_id = school.id school_name = school.name - teachers = Teacher.objects.filter(school=school).order_by("new_user__last_name", "new_user__first_name") + teachers = Teacher.objects.filter(school=school).order_by( + "new_user__last_name", "new_user__first_name" + ) assert len(teachers) == 3 # one of the remaining teachers should be admin (the second in our case, as it's alphabetical) @@ -760,7 +776,9 @@ def test_delete_account_admin(self, mock_send_dotdigital_email: Mock): self.assertEqual(mock_send_dotdigital_email.call_count, 2) # 2 teachers left - teachers = Teacher.objects.filter(school=school).order_by("new_user__last_name", "new_user__first_name") + teachers = Teacher.objects.filter(school=school).order_by( + "new_user__last_name", "new_user__first_name" + ) assert len(teachers) == 2 # teacher2 should still be admin, teacher4 is not passed admin role because there is teacher2 @@ -772,7 +790,9 @@ def test_delete_account_admin(self, mock_send_dotdigital_email: Mock): # delete teacher4 anonymise(user4) - teachers = Teacher.objects.filter(school=school).order_by("new_user__last_name", "new_user__first_name") + teachers = Teacher.objects.filter(school=school).order_by( + "new_user__last_name", "new_user__first_name" + ) assert len(teachers) == 1 u = User.objects.get(id=usrid2) assert u.new_teacher.is_admin @@ -830,13 +850,17 @@ def test_logged_in_as_admin_check(self): c.logout() @patch("common.helpers.emails.send_dotdigital_email") - def test_registrations_increment_data(self, mock_send_dotdigital_email: Mock): + def test_registrations_increment_data( + self, mock_send_dotdigital_email: Mock + ): c = Client() total_activity = TotalActivity.objects.get(id=1) teacher_registration_count = total_activity.teacher_registrations student_registration_count = total_activity.student_registrations - independent_registration_count = total_activity.independent_registrations + independent_registration_count = ( + total_activity.independent_registrations + ) response = c.post( reverse("register"), @@ -856,7 +880,10 @@ def test_registrations_increment_data(self, mock_send_dotdigital_email: Mock): total_activity = TotalActivity.objects.get(id=1) - assert total_activity.teacher_registrations == teacher_registration_count + 1 + assert ( + total_activity.teacher_registrations + == teacher_registration_count + 1 + ) response = c.post( reverse("register"), @@ -878,7 +905,10 @@ def test_registrations_increment_data(self, mock_send_dotdigital_email: Mock): total_activity = TotalActivity.objects.get(id=1) - assert total_activity.independent_registrations == independent_registration_count + 1 + assert ( + total_activity.independent_registrations + == independent_registration_count + 1 + ) teacher_email, teacher_password = signup_teacher_directly() create_organisation_directly(teacher_email) @@ -894,7 +924,10 @@ def test_registrations_increment_data(self, mock_send_dotdigital_email: Mock): total_activity = TotalActivity.objects.get(id=1) - assert total_activity.student_registrations == student_registration_count + 3 + assert ( + total_activity.student_registrations + == student_registration_count + 3 + ) # CRON view tests @@ -913,8 +946,12 @@ def generic( secure=False, **extra, ): - wsgi_response = super().generic(method, path, data, content_type, secure, **extra) - assert 200 <= wsgi_response.status_code < 300, f"Response has error status code: {wsgi_response.status_code}" + wsgi_response = super().generic( + method, path, data, content_type, secure, **extra + ) + assert ( + 200 <= wsgi_response.status_code < 300 + ), f"Response has error status code: {wsgi_response.status_code}" return wsgi_response @@ -933,7 +970,9 @@ def setUp(self): indy_email, _, _ = create_independent_student_directly() self.teacher_user = User.objects.get(email=teacher_email) - self.teacher_user_profile = UserProfile.objects.get(user=self.teacher_user) + self.teacher_user_profile = UserProfile.objects.get( + user=self.teacher_user + ) self.indy_user = User.objects.get(email=indy_email) self.indy_user_profile = UserProfile.objects.get(user=self.indy_user) @@ -949,11 +988,17 @@ def send_verify_email_reminder( assert_called: bool, mock_send_dotdigital_email: Mock, ): - self.teacher_user.date_joined = timezone.now() - timedelta(days=days, hours=12) + self.teacher_user.date_joined = timezone.now() - timedelta( + days=days, hours=12 + ) self.teacher_user.save() - self.student_user.date_joined = timezone.now() - timedelta(days=days, hours=12) + self.student_user.date_joined = timezone.now() - timedelta( + days=days, hours=12 + ) self.student_user.save() - self.indy_user.date_joined = timezone.now() - timedelta(days=days, hours=12) + self.indy_user.date_joined = timezone.now() - timedelta( + days=days, hours=12 + ) self.indy_user.save() self.teacher_user_profile.is_verified = is_verified @@ -964,9 +1009,13 @@ def send_verify_email_reminder( self.client.get(reverse(view_name)) if assert_called: - mock_send_dotdigital_email.assert_any_call(ANY, [self.teacher_user.email], personalization_values=ANY) + mock_send_dotdigital_email.assert_any_call( + ANY, [self.teacher_user.email], personalization_values=ANY + ) - mock_send_dotdigital_email.assert_any_call(ANY, [self.indy_user.email], personalization_values=ANY) + mock_send_dotdigital_email.assert_any_call( + ANY, [self.indy_user.email], personalization_values=ANY + ) # Check only two emails are sent - the student should never be included. assert mock_send_dotdigital_email.call_count == 2 @@ -976,22 +1025,40 @@ def send_verify_email_reminder( mock_send_dotdigital_email.reset_mock() def test_first_verify_email_reminder_view(self): - self.send_verify_email_reminder(6, False, "first-verify-email-reminder", False) - self.send_verify_email_reminder(7, False, "first-verify-email-reminder", True) - self.send_verify_email_reminder(7, True, "first-verify-email-reminder", False) - self.send_verify_email_reminder(8, False, "first-verify-email-reminder", False) + self.send_verify_email_reminder( + 6, False, "first-verify-email-reminder", False + ) + self.send_verify_email_reminder( + 7, False, "first-verify-email-reminder", True + ) + self.send_verify_email_reminder( + 7, True, "first-verify-email-reminder", False + ) + self.send_verify_email_reminder( + 8, False, "first-verify-email-reminder", False + ) def test_second_verify_email_reminder_view(self): - self.send_verify_email_reminder(13, False, "second-verify-email-reminder", False) - self.send_verify_email_reminder(14, False, "second-verify-email-reminder", True) - self.send_verify_email_reminder(14, True, "second-verify-email-reminder", False) - self.send_verify_email_reminder(15, False, "second-verify-email-reminder", False) + self.send_verify_email_reminder( + 13, False, "second-verify-email-reminder", False + ) + self.send_verify_email_reminder( + 14, False, "second-verify-email-reminder", True + ) + self.send_verify_email_reminder( + 14, True, "second-verify-email-reminder", False + ) + self.send_verify_email_reminder( + 15, False, "second-verify-email-reminder", False + ) def test_anonymise_unverified_accounts_view(self): now = timezone.now() for user in [self.teacher_user, self.indy_user, self.student_user]: - user.date_joined = now - timedelta(days=USER_DELETE_UNVERIFIED_ACCOUNT_DAYS + 1) + user.date_joined = now - timedelta( + days=USER_DELETE_UNVERIFIED_ACCOUNT_DAYS + 1 + ) user.save() for user_profile in [self.teacher_user_profile, self.indy_user_profile]: @@ -1056,7 +1123,9 @@ def anonymise_unverified_users( new_user=indy_user, ) - activity_today = DailyActivity.objects.get_or_create(date=datetime.now().date())[0] + activity_today = DailyActivity.objects.get_or_create( + date=datetime.now().date() + )[0] daily_teacher_count = activity_today.anonymised_unverified_teachers daily_indy_count = activity_today.anonymised_unverified_independents @@ -1079,16 +1148,30 @@ def anonymise_unverified_users( assert indy_user_active == assert_active assert student_user_active - activity_today = DailyActivity.objects.get_or_create(date=datetime.now().date())[0] + activity_today = DailyActivity.objects.get_or_create( + date=datetime.now().date() + )[0] total_activity = TotalActivity.objects.get(id=1) if not teacher_user_active: - assert activity_today.anonymised_unverified_teachers == daily_teacher_count + 1 - assert total_activity.anonymised_unverified_teachers == total_teacher_count + 1 + assert ( + activity_today.anonymised_unverified_teachers + == daily_teacher_count + 1 + ) + assert ( + total_activity.anonymised_unverified_teachers + == total_teacher_count + 1 + ) if not indy_user_active: - assert activity_today.anonymised_unverified_independents == daily_indy_count + 1 - assert total_activity.anonymised_unverified_independents == total_indy_count + 1 + assert ( + activity_today.anonymised_unverified_independents + == daily_indy_count + 1 + ) + assert ( + total_activity.anonymised_unverified_independents + == total_indy_count + 1 + ) teacher_user.delete() indy_user.delete() diff --git a/portal/tests/utils/aimmo_games.py b/portal/tests/utils/aimmo_games.py deleted file mode 100644 index b709653ec..000000000 --- a/portal/tests/utils/aimmo_games.py +++ /dev/null @@ -1,30 +0,0 @@ -from aimmo.models import Game -from common.models import Class - - -def generate_name(): - name = "Game %d" % generate_name.next_id - - generate_name.next_id += 1 - - return name - - -generate_name.next_id = 1 - - -def create_aimmo_game_directly(klass: Class, worksheet_id: int) -> Game: - """Generate an aimmo game with the details given. - - Args: - klass (Class): The instance of the class. - worksheet_id (int): The id of the worksheet. - - Returns: - game: Game: The game model instance. - """ - name = generate_name() - - game = Game.objects.create(name=name, game_class=klass, worksheet_id=worksheet_id) - - return game diff --git a/portal/urls.py b/portal/urls.py index 970c19048..b8f422ca5 100644 --- a/portal/urls.py +++ b/portal/urls.py @@ -1,4 +1,3 @@ -from aimmo.urls import HOMEPAGE_REGEX from common.permissions import teacher_verified from django.conf.urls import include, url from django.http import HttpResponse @@ -25,8 +24,10 @@ from portal.helpers.regexes import ACCESS_CODE_REGEX, JWT_REGEX from portal.views import cron from portal.views.about import about, contribute, getinvolved -from portal.views.admin import AdminChangePasswordDoneView, AdminChangePasswordView -from portal.views.aimmo.dashboard import StudentAimmoDashboard, TeacherAimmoDashboard +from portal.views.admin import ( + AdminChangePasswordDoneView, + AdminChangePasswordView, +) from portal.views.api import ( AnonymiseOrphanSchoolsView, InactiveUsersView, @@ -35,7 +36,10 @@ number_users_per_country, registered_users, ) -from portal.views.dotmailer import dotmailer_consent_form, process_newsletter_form +from portal.views.dotmailer import ( + dotmailer_consent_form, + process_newsletter_form, +) from portal.views.email import verify_email from portal.views.home import ( coding_club, @@ -109,7 +113,9 @@ js_info_dict = {"packages": ("conf.locale",)} two_factor_patterns = [ - url(r"^account/two_factor/setup/$", CustomSetupView.as_view(), name="setup"), + url( + r"^account/two_factor/setup/$", CustomSetupView.as_view(), name="setup" + ), url(r"^account/two_factor/qrcode/$", QRGeneratorView.as_view(), name="qr"), url( r"^account/two_factor/setup/complete/$", @@ -164,20 +170,11 @@ ] ), ), - url(HOMEPAGE_REGEX, include("aimmo.urls")), - url( - r"^teach/kurono/dashboard/$", - TeacherAimmoDashboard.as_view(), - name="teacher_aimmo_dashboard", - ), - url( - r"^play/kurono/dashboard/$", - StudentAimmoDashboard.as_view(), - name="student_aimmo_dashboard", - ), url( r"^favicon\.ico$", - RedirectView.as_view(url="/static/portal/img/favicon.ico", permanent=True), + RedirectView.as_view( + url="/static/portal/img/favicon.ico", permanent=True + ), ), url( r"^administration/password_change/$", @@ -189,7 +186,9 @@ AdminChangePasswordDoneView.as_view(), name="administration_password_change_done", ), - url(r"^users/inactive/", InactiveUsersView.as_view(), name="inactive_users"), + url( + r"^users/inactive/", InactiveUsersView.as_view(), name="inactive_users" + ), url( r"^locked_out/$", TemplateView.as_view(template_name="portal/locked_out.html"), @@ -267,7 +266,9 @@ url(r"^consent_form/$", dotmailer_consent_form, name="consent_form"), url( r"^verify_email/$", - TemplateView.as_view(template_name="portal/email_verification_needed.html"), + TemplateView.as_view( + template_name="portal/email_verification_needed.html" + ), name="email_verification", ), url( @@ -380,7 +381,9 @@ url(r"^contribute", contribute, name="contribute"), url(r"^terms", terms, name="terms"), url(r"^privacy-notice/$", privacy_notice, name="privacy_notice"), - url(r"^privacy-policy/$", privacy_notice, name="privacy_policy"), # Keeping this to route from old URL + url( + r"^privacy-policy/$", privacy_notice, name="privacy_policy" + ), # Keeping this to route from old URL url(r"^teach/dashboard/$", dashboard_manage, name="dashboard"), url( r"^teach/dashboard/kick/(?P[0-9]+)/$", diff --git a/portal/views/aimmo/__init__.py b/portal/views/aimmo/__init__.py deleted file mode 100644 index e69de29bb..000000000 diff --git a/portal/views/aimmo/dashboard.py b/portal/views/aimmo/dashboard.py deleted file mode 100644 index 0f6a97b4e..000000000 --- a/portal/views/aimmo/dashboard.py +++ /dev/null @@ -1,105 +0,0 @@ -from typing import Any, Dict, List, Optional - -from aimmo.models import Game -from aimmo.worksheets import WORKSHEETS, Worksheet, get_worksheets_excluding_id -from common.models import Class -from common.permissions import logged_in_as_student, logged_in_as_teacher -from common.utils import LoginRequiredNoErrorMixin -from django.contrib import messages -from django.contrib.auth.mixins import UserPassesTestMixin -from django.db.models import QuerySet -from django.urls import reverse_lazy -from django.views.generic.base import TemplateView -from django.views.generic.edit import CreateView - -from portal.forms.add_game import AddGameForm -from portal.strings.student_aimmo_dashboard import AIMMO_DASHBOARD_BANNER - - -class TeacherAimmoDashboard(LoginRequiredNoErrorMixin, UserPassesTestMixin, CreateView): - login_url = reverse_lazy("teacher_login") - form_class = AddGameForm - template_name = "portal/teach/teacher_aimmo_dashboard.html" - - def test_func(self) -> Optional[bool]: - return logged_in_as_teacher(self.request.user) - - def get_form(self, form_class=None): - teacher = self.request.user.new_teacher - non_admin_classes = teacher.class_teacher - admin_classes = Class.objects.filter(teacher__school=teacher.school) - classes = admin_classes if teacher.is_admin else non_admin_classes - if form_class is None: - form_class = self.get_form_class() - return form_class(classes, **self.get_form_kwargs()) - - def form_valid(self, form): - form.instance = Game( - game_class=form.cleaned_data["game_class"], created_by=self.request.user.userprofile.teacher - ) - return super().form_valid(form) - - def form_invalid(self, form: AddGameForm): - messages.warning( - self.request, - ", ".join(message for errors in form.errors.values() for message in errors), - ) - return super().form_invalid(form) - - def get_success_url(self): - return reverse_lazy("teacher_aimmo_dashboard") - - -class StudentAimmoDashboard(LoginRequiredNoErrorMixin, UserPassesTestMixin, TemplateView): - template_name = "portal/play/student_aimmo_dashboard.html" - - login_url = reverse_lazy("student_login_access_code") - - def test_func(self) -> Optional[bool]: - return logged_in_as_student(self.request.user) - - def get_context_data(self, **kwargs: Any) -> Dict[str, Any]: - student = self.request.user.new_student - klass = student.class_field - - if klass is None: - return {"BANNER": AIMMO_DASHBOARD_BANNER} - - aimmo_game = klass.active_game - if aimmo_game: - active_worksheet = WORKSHEETS.get(aimmo_game.worksheet_id) - inactive_worksheets = get_worksheets_excluding_id(active_worksheet.id) - - return { - "BANNER": AIMMO_DASHBOARD_BANNER, - "HERO_CARD": self._get_hero_card(active_worksheet, aimmo_game), - "CARD_LIST": {"cards": self._get_card_list(inactive_worksheets)}, - } - else: - return {"BANNER": AIMMO_DASHBOARD_BANNER} - - def _get_hero_card(self, active_worksheet: Worksheet, aimmo_game: Game) -> Dict[str, Any]: - return { - "image": active_worksheet.active_image_path, - "title": active_worksheet.name, - "description": active_worksheet.description, - "button1": { - "text": "Read challenge", - "url": active_worksheet.student_challenge_url, - }, - "button2": { - "text": "Play game", - "url": "kurono/play", - "url_args": aimmo_game.id, - }, - } - - def _get_card_list(self, inactive_worksheets: QuerySet) -> List[Dict[str, Any]]: - return [ - { - "image": inactive_worksheet.image_path, - "title": inactive_worksheet.name, - "description": inactive_worksheet.short_description, - } - for inactive_worksheet in inactive_worksheets - ] diff --git a/portal/views/student/play.py b/portal/views/student/play.py index 766d318cb..d82d07135 100644 --- a/portal/views/student/play.py +++ b/portal/views/student/play.py @@ -1,6 +1,5 @@ from typing import Any, Dict, List, Optional -from common.helpers.emails import NOTIFICATION_EMAIL, send_email from common.mail import campaign_ids, send_dotdigital_email from common.models import Student from common.permissions import ( @@ -22,7 +21,9 @@ from portal.forms.play import StudentJoinOrganisationForm -class SchoolStudentDashboard(LoginRequiredNoErrorMixin, UserPassesTestMixin, TemplateView): +class SchoolStudentDashboard( + LoginRequiredNoErrorMixin, UserPassesTestMixin, TemplateView +): template_name = "portal/play/student_dashboard.html" login_url = reverse_lazy("student_login_access_code") @@ -31,10 +32,9 @@ def test_func(self) -> Optional[bool]: def get_context_data(self, **kwargs: Any) -> Dict[str, Any]: """ - Gathers the context data required by the template. First, the student's scores - for the original Rapid Router levels is gathered, second, the student's scores - for any levels shared with them by their teacher, and third, the student's - Kurono game information if they have one. + Gathers the context data required by the template. First, the student's + scores for the original Rapid Router levels is gathered, second, + the student's scores for any levels shared with them by their teacher. """ # Get score data for all original levels levels = Level.objects.sorted_levels() @@ -42,29 +42,30 @@ def get_context_data(self, **kwargs: Any) -> Dict[str, Any]: context_data = _compute_rapid_router_scores(student, levels) - # Find any custom levels created by the teacher and shared with the student + # Find any custom levels created by the teacher and shared with the + # student klass = student.class_field teacher = klass.teacher.user custom_levels = student.new_user.shared.filter(owner=teacher) if custom_levels: - custom_levels_data = _compute_rapid_router_scores(student, custom_levels) + custom_levels_data = _compute_rapid_router_scores( + student, custom_levels + ) - context_data["total_custom_score"] = custom_levels_data["total_score"] - context_data["total_custom_available_score"] = custom_levels_data["total_available_score"] - - # Get Kurono game info if the class has a game linked to it - aimmo_game = klass.active_game - if aimmo_game: - active_worksheet = aimmo_game.worksheet - - context_data["worksheet_id"] = active_worksheet.id - context_data["worksheet_image"] = active_worksheet.image_path + context_data["total_custom_score"] = custom_levels_data[ + "total_score" + ] + context_data["total_custom_available_score"] = custom_levels_data[ + "total_available_score" + ] return context_data -class IndependentStudentDashboard(LoginRequiredNoErrorMixin, UserPassesTestMixin, TemplateView, FormView): +class IndependentStudentDashboard( + LoginRequiredNoErrorMixin, UserPassesTestMixin, TemplateView, FormView +): template_name = "portal/play/independent_student_dashboard.html" login_url = reverse_lazy("independent_student_login") @@ -81,7 +82,9 @@ def get_context_data(self, **kwargs: Any) -> Dict[str, Any]: ) -def _compute_rapid_router_scores(student: Student, levels: List[Level] or QuerySet) -> Dict[str, int]: +def _compute_rapid_router_scores( + student: Student, levels: List[Level] or QuerySet +) -> Dict[str, int]: """ Finds Rapid Router progress and score data for a specific student and a specific set of levels. This is used to show quick score data to the student on their @@ -100,9 +103,9 @@ def _compute_rapid_router_scores(student: Student, levels: List[Level] or QueryS num_completed = num_top_scores = total_available_score = 0 total_score = 0.0 # Get a QuerySet of best attempts for each level - best_attempts = Attempt.objects.filter(level__in=levels, student=student, is_best_attempt=True).select_related( - "level" - ) + best_attempts = Attempt.objects.filter( + level__in=levels, student=student, is_best_attempt=True + ).select_related("level") for level in levels: total_available_score += _get_max_score_for_level(level) @@ -110,7 +113,10 @@ def _compute_rapid_router_scores(student: Student, levels: List[Level] or QueryS # For each level, compare best attempt's score with level's max score and # increment variables as needed if best_attempts: - attempts_dict = {best_attempt.level.id: best_attempt for best_attempt in best_attempts} + attempts_dict = { + best_attempt.level.id: best_attempt + for best_attempt in best_attempts + } for level in levels: attempt = attempts_dict.get(level.id) @@ -140,7 +146,12 @@ def _get_max_score_for_level(level: Level) -> int: """ return ( 10 - if level.id > 12 and (level.disable_route_score or level.disable_algorithm_score or not level.episode) + if level.id > 12 + and ( + level.disable_route_score + or level.disable_algorithm_score + or not level.episode + ) else 20 ) diff --git a/portal/views/teacher/teach.py b/portal/views/teacher/teach.py index afdf370d1..e747832c1 100644 --- a/portal/views/teacher/teach.py +++ b/portal/views/teacher/teach.py @@ -6,7 +6,6 @@ from functools import partial, wraps from uuid import uuid4 -from aimmo.models import Game from common.helpers.emails import send_verification_email from common.helpers.generators import generate_access_code, generate_login_id, generate_password, get_hashed_login_id from common.models import Class, DailyActivity, JoinReleaseStudent, Student, Teacher, TotalActivity @@ -188,7 +187,6 @@ def teacher_view_class(request, access_code): @user_passes_test(logged_in_as_teacher, login_url=reverse_lazy("teacher_login")) def teacher_delete_class(request, access_code): klass = get_object_or_404(Class, access_code=access_code) - games = Game.objects.filter(game_class=klass) # check user authorised to see class check_teacher_authorised(request, klass.teacher) @@ -199,9 +197,6 @@ def teacher_delete_class(request, access_code): ) return HttpResponseRedirect(reverse_lazy("view_class", kwargs={"access_code": access_code})) - for game in games: - game.is_archived = True - game.save() klass.anonymise() return HttpResponseRedirect(reverse_lazy("dashboard") + "#classes") diff --git a/setup.py b/setup.py index 305d580b2..f27548660 100644 --- a/setup.py +++ b/setup.py @@ -31,8 +31,7 @@ "django-recaptcha==2.0.6", "pyyaml==5.4.1", "importlib-metadata==4.13.0", - "rapid-router>=4", - "aimmo>=2", + "rapid-router>=6", "reportlab==3.6.13", "django-formtools==2.2", "django-otp==1.0.2", # we needed to fix this due to a wide ranged dependency in django-two-factor-auth