diff --git a/Pipfile b/Pipfile index b8084ab87..f1ebe79a3 100644 --- a/Pipfile +++ b/Pipfile @@ -44,6 +44,7 @@ django-admin-multiple-choice-list-filter = "*" django-npm = "*" fpdf2 = "==2.5.5" pymemcache = "*" +black = ">=24" weasyprint = "*" tesseract = "==0.1.3" pytesseract = "*" diff --git a/Pipfile.lock b/Pipfile.lock index ad0dc0f21..1e001becc 100644 --- a/Pipfile.lock +++ b/Pipfile.lock @@ -1,7 +1,7 @@ { "_meta": { "hash": { - "sha256": "1fd8697752dcaa124937c8bcb59416b859aead8b3660241e778862340e682e2d" + "sha256": "d76fad73be0fb8a593f6862925edd87bc25d8c8547f4672cd3a62867e2f76310" }, "pipfile-spec": 6, "requires": { @@ -105,7 +105,7 @@ "sha256:74e3d1928edc070d21748185c46e3fb33490f22f52a3addee9aee0f4f7781051", "sha256:b80878c9f40111313e55da8ba20bdba06d8fa3969fc68304167741bbf9e082ed" ], - "markers": "python_version >= '3.6'", + "markers": "python_full_version >= '3.6.0'", "version": "==4.12.3" }, "billiard": { @@ -116,6 +116,35 @@ "markers": "python_version >= '3.7'", "version": "==4.2.0" }, + "black": { + "hashes": [ + "sha256:057c3dc602eaa6fdc451069bd027a1b2635028b575a6c3acfd63193ced20d9c8", + "sha256:08654d0797e65f2423f850fc8e16a0ce50925f9337fb4a4a176a7aa4026e63f8", + "sha256:163baf4ef40e6897a2a9b83890e59141cc8c2a98f2dda5080dc15c00ee1e62cd", + "sha256:1e08fb9a15c914b81dd734ddd7fb10513016e5ce7e6704bdd5e1251ceee51ac9", + "sha256:4dd76e9468d5536abd40ffbc7a247f83b2324f0c050556d9c371c2b9a9a95e31", + "sha256:4f9de21bafcba9683853f6c96c2d515e364aee631b178eaa5145fc1c61a3cc92", + "sha256:61a0391772490ddfb8a693c067df1ef5227257e72b0e4108482b8d41b5aee13f", + "sha256:6981eae48b3b33399c8757036c7f5d48a535b962a7c2310d19361edeef64ce29", + "sha256:7e53a8c630f71db01b28cd9602a1ada68c937cbf2c333e6ed041390d6968faf4", + "sha256:810d445ae6069ce64030c78ff6127cd9cd178a9ac3361435708b907d8a04c693", + "sha256:93601c2deb321b4bad8f95df408e3fb3943d85012dddb6121336b8e24a0d1218", + "sha256:992e451b04667116680cb88f63449267c13e1ad134f30087dec8527242e9862a", + "sha256:9db528bccb9e8e20c08e716b3b09c6bdd64da0dd129b11e160bf082d4642ac23", + "sha256:a0057f800de6acc4407fe75bb147b0c2b5cbb7c3ed110d3e5999cd01184d53b0", + "sha256:ba15742a13de85e9b8f3239c8f807723991fbfae24bad92d34a2b12e81904982", + "sha256:bce4f25c27c3435e4dace4815bcb2008b87e167e3bf4ee47ccdc5ce906eb4894", + "sha256:ca610d29415ee1a30a3f30fab7a8f4144e9d34c89a235d81292a1edb2b55f540", + "sha256:d533d5e3259720fdbc1b37444491b024003e012c5173f7d06825a77508085430", + "sha256:d84f29eb3ee44859052073b7636533ec995bd0f64e2fb43aeceefc70090e752b", + "sha256:e37c99f89929af50ffaf912454b3e3b47fd64109659026b678c091a4cd450fb2", + "sha256:e8a6ae970537e67830776488bca52000eaa37fa63b9988e8c487458d9cd5ace6", + "sha256:faf2ee02e6612577ba0181f4347bcbcf591eb122f7841ae5ba233d12c39dcb4d" + ], + "index": "pypi", + "markers": "python_version >= '3.8'", + "version": "==24.2.0" + }, "blinker": { "hashes": [ "sha256:c3f865d4d54db7abc53758a01601cf343fe55b84c1de4e3fa910e420b438d5b9", @@ -126,19 +155,21 @@ }, "boto3": { "hashes": [ - "sha256:35bcbecf1b5d3620c93f0062d2994177f8bda25a9d2cba144d6462793c16065b", - "sha256:476896e70d36c9134d4125834280c597c17b54bff4902baf2e5fcde74f8acec8" + "sha256:49eb215e4142d441e26eedaf5d0b43065200f0849d82c904bc9a62d1328016cd", + "sha256:81d026ed8c8305b880c71f9f287f9b745b52bd358a91cfc133844c907db4d7ee" ], "index": "pypi", - "version": "==1.34.39" + "markers": "python_version >= '3.8'", + "version": "==1.34.40" }, "botocore": { "hashes": [ - "sha256:9f00bd5e4698bcdd37ce6e224a896baf58d209678ed92834944b767de9061cc5", - "sha256:e175360445424b83b0e28ae20d301b99cf44ff2c9d5ab1d8670899bec05a9753" + "sha256:a3edd774653a61a1b211e4ea88cdb1c2655ffcc7660ba77b41a4027b097d145d", + "sha256:cb794bdb5b3d41845749a182ec93cb1453560e52b97ae0ab43ace81deb011f6d" ], "index": "pypi", - "version": "==1.34.39" + "markers": "python_version >= '3.8'", + "version": "==1.34.40" }, "brotli": { "hashes": [ @@ -236,7 +267,7 @@ "sha256:870cc71d737c0200c397290d730344cc991d13a057534353d124c9380267aab9", "sha256:9da4ea0118d232ce97dff5ed4974587fb1c0ff5c10042eb15278487cdd27d1af" ], - "index": "pypi", + "markers": "python_version >= '3.8'", "version": "==5.3.6" }, "certifi": { @@ -313,7 +344,7 @@ "sha256:0ce53507a7da7b148eaa454526e0e05f7da5e5d1c23440e4886cf146981d8420", "sha256:2253334ac76f67cba68c2072273f7e0e67dbdac77eeb7e318f511d2f9a53c5e4" ], - "index": "pypi", + "markers": "python_version >= '3.7'", "version": "==4.0.0" }, "channels-redis": { @@ -322,6 +353,7 @@ "sha256:2c5b944a39bd984b72aa8005a3ae11637bf29b5092adeb91c9aad4ab819a8ac4" ], "index": "pypi", + "markers": "python_version >= '3.8'", "version": "==4.2.0" }, "charset-normalizer": { @@ -417,7 +449,7 @@ "sha256:fd1abc0d89e30cc4e02e4064dc67fcc51bd941eb395c502aac3ec19fab46b519", "sha256:ff8fa367d09b717b2a17a052544193ad76cd49979c805768879cb63d9ca50561" ], - "markers": "python_version >= '3.7'", + "markers": "python_full_version >= '3.7.0'", "version": "==3.3.2" }, "click": { @@ -433,7 +465,7 @@ "sha256:a0713dc7a1de3f06bc0df5a9567ad19ead2d3d5689b434768a6145bff77c0667", "sha256:f184f0d851d96b6d29297354ed981b7dd71df7ff500d82fa6d11f0856bee8035" ], - "markers": "python_full_version >= '3.6.2' and python_version < '4.0'", + "markers": "python_full_version >= '3.6.2' and python_full_version < '4.0.0'", "version": "==0.3.0" }, "click-plugins": { @@ -536,10 +568,10 @@ }, "daphne": { "hashes": [ - "sha256:a288ece46012b6b719c37150be67c69ebfca0793a8521bf821533bad983179b2", - "sha256:cce9afc8f49a4f15d4270b8cfb0e0fe811b770a5cc795474e97e4da287497666" + "sha256:7228cd6a3ca5a9b11c9a1c1c0414dab1bfb4ddc55ff234b545db8d71f6c24938", + "sha256:882fab39d0b90c6b2709b38116c95f660b6cf236600115dd7c13161fb98b3448" ], - "version": "==4.0.0" + "version": "==4.1.0" }, "defusedxml": { "hashes": [ @@ -554,7 +586,7 @@ "sha256:5dd5b787c3ba39637610fe700f54bf158e33560ea0dba600c19921e7ff926ec5", "sha256:aaee9fb0fb4ebd4311520887ad2e33313d368846607f82a9a0ed461cd4c35b18" ], - "index": "pypi", + "markers": "python_version >= '3.6'", "version": "==3.2.24" }, "django-admin-multiple-choice-list-filter": { @@ -579,6 +611,7 @@ "sha256:accc64255af7bb45df6b518673facbbaf572418c26e3c3579a0cf4e9c4ef1183" ], "index": "pypi", + "markers": "python_version >= '3.8'", "version": "==24.1" }, "django-celery-beat": { @@ -595,6 +628,7 @@ "sha256:e09b7dcb8417b743234dfc57c95a7c1d1d87a88844abd13b4c5387f807b31bf6" ], "index": "pypi", + "markers": "python_version >= '3.8'", "version": "==4.3.0" }, "django-elasticsearch-dsl": { @@ -611,6 +645,7 @@ "sha256:ff6940cf37e07d6d0c4ac28c5420c8cfc478b62541473dba4aa02d600f7db9fc" ], "index": "pypi", + "markers": "python_version >= '3.7'", "version": "==5.0.13" }, "django-maintenance-mode": { @@ -654,6 +689,7 @@ "sha256:d047a31cf94d83ef1465d7543ca66c6fc16695559b5f8d814d1b51df15110b92" ], "index": "pypi", + "markers": "python_version >= '3.7'", "version": "==4.1.0" }, "django-redis": { @@ -662,6 +698,7 @@ "sha256:ebc88df7da810732e2af9987f7f426c96204bf89319df4c6da6ca9a2942edd5b" ], "index": "pypi", + "markers": "python_version >= '3.6'", "version": "==5.4.0" }, "django-registration": { @@ -670,6 +707,7 @@ "sha256:fa76df481189794f47eb73043ee5cc9bfb0963194b901d7bd8cf914beab1ddd0" ], "index": "pypi", + "markers": "python_version >= '3.7'", "version": "==3.4" }, "django-robots": { @@ -678,6 +716,7 @@ "sha256:f86bcc3d16d7d7c2a4e37af6063cb4785f50ae16943f82248b48c9e7ac034f1d" ], "index": "pypi", + "markers": "python_version >= '3.7'", "version": "==6.1" }, "django-simple-captcha": { @@ -694,6 +733,7 @@ "sha256:51b36af28cc5813b98d5f3dfe7459af638d84428c8df4a03990c7d74d1bea4e5" ], "index": "pypi", + "markers": "python_version >= '3.7'", "version": "==1.14.2" }, "django-tabular-export": { @@ -718,6 +758,7 @@ "sha256:beb4d27cdacd4f8b00c90378f02898cb448e9f01a1a8a65eff4c38ca3c8edbc9" ], "index": "pypi", + "markers": "python_version >= '3.8'", "version": "==3.7.1" }, "elasticsearch": { @@ -726,6 +767,7 @@ "sha256:5920df0ab2630778680376d86bea349dc99860977eec9b6d2bd0860f337313f2" ], "index": "pypi", + "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3' and python_version < '4'", "version": "==7.13.4" }, "elasticsearch-dsl": { @@ -766,6 +808,7 @@ "sha256:da880a76322db7a879c848a0771e129338e0a680a9f695fd9a3e7a6ac82b45e1" ], "index": "pypi", + "markers": "python_version >= '3.8'", "version": "==19.13.0" }, "flask": { @@ -1079,6 +1122,7 @@ "sha256:88ec8bff1d634f98e61b9f65bc4bf3cd918a90806c6f5c48bc5603849ec81033" ], "index": "pypi", + "markers": "python_version >= '3.5'", "version": "==21.2.0" }, "h11": { @@ -1209,6 +1253,7 @@ "sha256:fa45f7d771094b8145af10db74704ab0f698adb682fbf3721d8090f90e42cc49" ], "index": "pypi", + "markers": "python_version >= '3.7'", "version": "==2.3.2" }, "hpack": { @@ -1247,6 +1292,7 @@ "sha256:9ecdbbd083b06798ae1e86adcbfe8ab1479cf864e4ee30fe4e46a003d12491ca", "sha256:c05567e9c24a6b9faaa835c4821bad0590fbb9d5779e7caa6e1cc4978e7eb24f" ], + "markers": "python_version >= '3.5'", "version": "==3.6" }, "incremental": { @@ -1302,15 +1348,17 @@ "sha256:30e470f1a6b49c70dc6f6d13c3e4cc4e178aa6c469ceb6bcd55645385fc84b93" ], "index": "pypi", + "markers": "python_version >= '3.8'", "version": "==5.3.5" }, "locust": { "hashes": [ - "sha256:35ae933cf5692af07f1e7c210633c9dc8b0e4c0d1bbf3b4f7382110ac4dedc78", - "sha256:b8132159bc527825d5f8f0869e17b0014258b927b57d8e184a2badb596ba5e6f" + "sha256:6cc729729e5ebf5852fc9d845302cfcf0ab0132f198e68b3eb0c88b438b6a863", + "sha256:96013a460a4b4d6d4fd46c70e6ff1fd2b6e03b48ddb1b48d1513d3134ba2cecf" ], "index": "pypi", - "version": "==2.22.0" + "markers": "python_version >= '3.8'", + "version": "==2.23.1" }, "markdown": { "hashes": [ @@ -1318,6 +1366,7 @@ "sha256:e1ac7b3dc550ee80e602e71c1d168002f062e49f1b11e26a36264dafd4df2ef8" ], "index": "pypi", + "markers": "python_version >= '3.8'", "version": "==3.5.2" }, "markdown-it-py": { @@ -1408,6 +1457,7 @@ "sha256:8fccb480c43d3e99a00087634c06dd02b0d50fbf088b380de5a41a015ec239e1" ], "index": "pypi", + "markers": "python_version >= '3.8'", "version": "==10.2.0" }, "msgpack": { @@ -1472,6 +1522,14 @@ "markers": "python_version >= '3.8'", "version": "==1.0.7" }, + "mypy-extensions": { + "hashes": [ + "sha256:4392f6c0eb8a5668a69e23d168ffa70f0be9ccfd32b5cc2d26a34ae5b844552d", + "sha256:75dbf8955dc00442a438fc4d0666508a9a97b6bd41aa2f0ffe9d2f2725af0782" + ], + "markers": "python_version >= '3.5'", + "version": "==1.0.0" + }, "nh3": { "hashes": [ "sha256:0d02d0ff79dfd8208ed25a39c12cbda092388fff7f1662466e27d97ad011b770", @@ -1500,6 +1558,7 @@ "sha256:e47805627aebcf860edb4edf7987b1309c1b3632f3750538ed962bbcc3bd7449" ], "index": "pypi", + "markers": "python_version >= '3.6'", "version": "==3.0.10" }, "outcome": { @@ -1518,6 +1577,14 @@ "markers": "python_version >= '3.7'", "version": "==23.2" }, + "pathspec": { + "hashes": [ + "sha256:a0d503e138a4c123b27490a4f7beda6a01c6f288df0e4a8b79c7eb0dc7b4cc08", + "sha256:a482d51503a1ab33b1c67a6c3813a26953dbdc71c31dacaef9a838c4e29f5712" + ], + "markers": "python_version >= '3.8'", + "version": "==0.12.1" + }, "pillow": { "hashes": [ "sha256:0304004f8067386b477d20a518b50f3fa658a28d44e4116970abfcd94fac34a8", @@ -1589,9 +1656,17 @@ "sha256:f379abd2f1e3dddb2b61bc67977a6b5a0a3f7485538bcc6f39ec76163891ee48", "sha256:fe4c15f6c9285dc54ce6553a3ce908ed37c8f3825b5a51a15c91442bb955b868" ], - "index": "pypi", + "markers": "python_version >= '3.8'", "version": "==10.2.0" }, + "platformdirs": { + "hashes": [ + "sha256:0614df2a2f37e1a662acbd8e2b25b92ccf8632929bc6d43467e17fe89c75e068", + "sha256:ef0cc731df711022c174543cb70a9b5bd22e5a9337c8624ef2c2ceb8ddad8768" + ], + "markers": "python_version >= '3.8'", + "version": "==4.2.0" + }, "pluggy": { "hashes": [ "sha256:7db9f7b503d67d1c5b95f59773ebb58a8c1c288129a88665838012cfb07b8981", @@ -1620,7 +1695,7 @@ "sha256:3527b7af26106cbc65a040bcc84839a3566ec1b051bb0bfe953631e704b0ff7d", "sha256:a11a29cb3bf0a28a387fe5122cdb649816a957cd9261dcedf8c9f1fef33eacf6" ], - "markers": "python_version >= '3.7'", + "markers": "python_full_version >= '3.7.0'", "version": "==3.0.43" }, "psutil": { @@ -1662,6 +1737,7 @@ "sha256:ff432630e510709564c01dafdbe996cb552e0b9f3f065eb89bdce5bd31fabf4c" ], "index": "pypi", + "markers": "python_version >= '3.7'", "version": "==2.9.9" }, "pyasn1": { @@ -1670,6 +1746,7 @@ "sha256:6d391a96e59b23130a5cfa74d6fd7f388dbbe26cc8f1edf39fdddf08d9d6676c" ], "index": "pypi", + "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4, 3.5'", "version": "==0.5.1" }, "pyasn1-modules": { @@ -1802,6 +1879,7 @@ "sha256:ae67172eb955bd47d463cba27a9b8a69ec90f4df02499ad5e370c8d336394548" ], "index": "pypi", + "markers": "python_full_version >= '3.8.1'", "version": "==1.20.0" }, "pylibmc": { @@ -1831,6 +1909,7 @@ "sha256:f536d73632007358796654ab088d65c55a1a4368a85cfd7c956d2100e2cd8d89" ], "index": "pypi", + "markers": "python_version >= '3.6'", "version": "==1.6.3" }, "pymemcache": { @@ -1839,6 +1918,7 @@ "sha256:f507bc20e0dc8d562f8df9d872107a278df049fa496805c1431b926f3ddd0eab" ], "index": "pypi", + "markers": "python_version >= '3.7'", "version": "==4.0.0" }, "pyopenssl": { @@ -1879,6 +1959,7 @@ "sha256:f1c3a8b0f07fd01a1085d451f5b8315be6eec1d5577a6796d46dc7a62bd4120f" ], "index": "pypi", + "markers": "python_version >= '3.7'", "version": "==0.3.10" }, "pytest": { @@ -2030,6 +2111,7 @@ "sha256:0dab495cd5753069d3bc650a0dde8a8f9edde16fc5691b689a566eda58100d0f", "sha256:ed4802971884ae19d640775ba3b03aa2e7bd5e8fb8dfaed2decce4d0fc48391f" ], + "markers": "python_version >= '3.7'", "version": "==5.0.1" }, "requests": { @@ -2038,6 +2120,7 @@ "sha256:942c5a758f98d790eaed1a29cb6eefc7ffb0d1cf7af05c3d2791656dbd6ad1e1" ], "index": "pypi", + "markers": "python_version >= '3.7'", "version": "==2.31.0" }, "rich": { @@ -2079,11 +2162,11 @@ }, "sentry-sdk": { "hashes": [ - "sha256:3c2b027979bb400cd65a47970e64f8cef8acda86b288a27f42a98692505086cd", - "sha256:73383f28311ae55602bb6cc3b013830811135ba5521e41333a6e68f269413502" + "sha256:657abae98b0050a0316f0873d7149f951574ae6212f71d2e3a1c4c88f62d6456", + "sha256:ac5cf56bb897ec47135d239ddeedf7c1c12d406fb031a4c0caa07399ed014d7e" ], "index": "pypi", - "version": "==1.40.3" + "version": "==1.40.4" }, "service-identity": { "hashes": [ @@ -2094,11 +2177,11 @@ }, "setuptools": { "hashes": [ - "sha256:385eb4edd9c9d5c17540511303e39a147ce2fc04bc55289c322b9e5904fe2c05", - "sha256:be1af57fc409f93647f2e8e4573a142ed38724b8cdd389706a867bb4efcf1e78" + "sha256:850894c4195f09c4ed30dba56213bf7c3f21d86ed6bdaafb5df5972593bfc401", + "sha256:c054629b81b946d63a9c6e732bc8b2513a7c3ea645f11d0139a2191d735c60c6" ], "markers": "python_version >= '3.8'", - "version": "==69.0.3" + "version": "==69.1.0" }, "setuptools-scm": { "hashes": [ @@ -2106,6 +2189,7 @@ "sha256:b5f43ff6800669595193fd09891564ee9d1d7dcb196cab4b2506d53a2e1c95c7" ], "index": "pypi", + "markers": "python_version >= '3.8'", "version": "==8.0.4" }, "shellingham": { @@ -2202,7 +2286,7 @@ "sha256:4ae8bce12999a35f7fe6443e7f1893e6fe09588c8d2bed9c35cdce8ff2d5b444", "sha256:987847a0790a2c597197613686e2784fd54167df3a55d0fb17c8412305d76ce5" ], - "index": "pypi", + "markers": "python_full_version >= '3.8.0'", "version": "==23.10.0" }, "txaio": { @@ -2234,18 +2318,18 @@ }, "tzdata": { "hashes": [ - "sha256:aa3ace4329eeacda5b7beb7ea08ece826c28d761cda36e747cfbf97996d39bf3", - "sha256:dd54c94f294765522c77399649b4fefd95522479a664a0cec87f41bebc6148c9" + "sha256:2674120f8d891909751c38abcdfd386ac0a5a1127954fbc332af6b5ceae07efd", + "sha256:9068bc196136463f5245e51efda838afa15aaeca9903f49050dfa2679db4d252" ], "markers": "python_version >= '2'", - "version": "==2023.4" + "version": "==2024.1" }, "urllib3": { "hashes": [ "sha256:34b97092d7e0a3a8cf7cd10e386f401b3737364026c45e622aa02903dffe0f07", "sha256:f8ecc1bba5667413457c529ab955bf8c67b45db799d159066261719e328580a0" ], - "markers": "python_version >= '3.10'", + "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4, 3.5'", "version": "==1.26.18" }, "vine": { @@ -2265,11 +2349,12 @@ }, "weasyprint": { "hashes": [ - "sha256:0c0cdd617a78699262b80026e67fa1692e3802cfa966395436eeaf6f787dd126", - "sha256:3e98eedcc1c5a14cb310c293c6d59a479f59a13f0d705ff07106482827fa5705" + "sha256:1dd5e929389b7ebcbff3088da7af13ae7ab201dce3a2faca7832b1dd5cec60ea", + "sha256:d91b11a05426fef1d63de826f30a80521d48c6a356455d338c2c429989fa586d" ], "index": "pypi", - "version": "==60.2" + "markers": "python_version >= '3.8'", + "version": "==61.0" }, "webencodings": { "hashes": [ @@ -2292,6 +2377,7 @@ "sha256:b1f9db9bf67dc183484d760b99f4080185633136a273a03f6436034a41064146" ], "index": "pypi", + "markers": "python_version >= '3.8'", "version": "==6.6.0" }, "wsproto": { @@ -2299,7 +2385,7 @@ "sha256:ad565f26ecb92588a3e43bc3d96164de84cd9902482b130d0ddbaa9664a85065", "sha256:b9acddd652b585d75b20477888c56642fdade28bdfd3579aa24a4d2c037dd736" ], - "markers": "python_version >= '3.7'", + "markers": "python_full_version >= '3.7.0'", "version": "==1.2.0" }, "xlsxwriter": { @@ -2552,6 +2638,7 @@ "sha256:fe558371c1bdf3b8fa03e097c523fb9645b8730399c14fe7721ee9c9e2a545d3" ], "index": "pypi", + "markers": "python_version >= '3.8'", "version": "==7.4.1" }, "distlib": { @@ -2566,7 +2653,7 @@ "sha256:5dd5b787c3ba39637610fe700f54bf158e33560ea0dba600c19921e7ff926ec5", "sha256:aaee9fb0fb4ebd4311520887ad2e33313d368846607f82a9a0ed461cd4c35b18" ], - "index": "pypi", + "markers": "python_version >= '3.6'", "version": "==3.2.24" }, "django-debug-toolbar": { @@ -2575,6 +2662,7 @@ "sha256:e09b7dcb8417b743234dfc57c95a7c1d1d87a88844abd13b4c5387f807b31bf6" ], "index": "pypi", + "markers": "python_version >= '3.8'", "version": "==4.3.0" }, "django-extensions": { @@ -2583,6 +2671,7 @@ "sha256:9600b7562f79a92cbf1fde6403c04fee314608fefbb595502e34383ae8203401" ], "index": "pypi", + "markers": "python_version >= '3.6'", "version": "==3.2.3" }, "filelock": { @@ -2595,11 +2684,11 @@ }, "identify": { "hashes": [ - "sha256:161558f9fe4559e1557e1bff323e8631f6a0e4837f7497767c1782832f16b62d", - "sha256:d40ce5fcd762817627670da8a7d8d8e65f24342d14539c59488dc603bf662e34" + "sha256:a4316013779e433d08b96e5eabb7f641e6c7942e4ab5d4c509ebd2e7a8994aed", + "sha256:ee17bc9d499899bc9eaec1ac7bf2dc9eedd480db9d88b96d123d3b64a9d34f5d" ], "markers": "python_version >= '3.8'", - "version": "==2.5.33" + "version": "==2.5.34" }, "invoke": { "hashes": [ @@ -2607,6 +2696,7 @@ "sha256:ee6cbb101af1a859c7fe84f2a264c059020b0cb7fe3535f9424300ab568f6bd5" ], "index": "pypi", + "markers": "python_version >= '3.6'", "version": "==2.2.0" }, "nodeenv": { @@ -2627,11 +2717,19 @@ }, "pre-commit": { "hashes": [ - "sha256:c255039ef399049a5544b6ce13d135caba8f2c28c3b4033277a788f434308376", - "sha256:d30bad9abf165f7785c15a21a1f46da7d0677cb00ee7ff4c579fd38922efe15d" + "sha256:9fe989afcf095d2c4796ce7c553cf28d4d4a9b9346de3cda079bcf40748454a4", + "sha256:c90961d8aa706f75d60935aba09469a6b0bcb8345f127c3fbee4bdc5f114cf4b" ], "index": "pypi", - "version": "==3.6.0" + "markers": "python_version >= '3.9'", + "version": "==3.6.1" + }, + "pytz": { + "hashes": [ + "sha256:2a29735ea9c18baf14b448846bde5a48030ed267578472d8955cd0e7443a9812", + "sha256:328171f4e3623139da4983451950b28e95ac706e13f3f2630a879749e7a8b319" + ], + "version": "==2024.1" }, "pyyaml": { "hashes": [ @@ -2692,11 +2790,11 @@ }, "setuptools": { "hashes": [ - "sha256:385eb4edd9c9d5c17540511303e39a147ce2fc04bc55289c322b9e5904fe2c05", - "sha256:be1af57fc409f93647f2e8e4573a142ed38724b8cdd389706a867bb4efcf1e78" + "sha256:850894c4195f09c4ed30dba56213bf7c3f21d86ed6bdaafb5df5972593bfc401", + "sha256:c054629b81b946d63a9c6e732bc8b2513a7c3ea645f11d0139a2191d735c60c6" ], "markers": "python_version >= '3.8'", - "version": "==69.0.3" + "version": "==69.1.0" }, "sqlparse": { "hashes": [ diff --git a/concordia/admin/__init__.py b/concordia/admin/__init__.py index 552c9325c..454cff328 100644 --- a/concordia/admin/__init__.py +++ b/concordia/admin/__init__.py @@ -30,17 +30,20 @@ Banner, Campaign, CampaignRetirementProgress, + Card, + CardFamily, CarouselSlide, + Guide, Item, Project, Resource, ResourceFile, - SimpleContentBlock, SimplePage, SiteReport, Tag, Topic, Transcription, + TutorialCard, UserAssetTagCollection, UserProfileActivity, ) @@ -61,6 +64,7 @@ AssetCampaignListFilter, AssetCampaignStatusListFilter, AssetProjectListFilter, + CardCampaignListFilter, ItemCampaignListFilter, ItemCampaignStatusListFilter, ItemProjectListFilter, @@ -86,9 +90,10 @@ from .forms import ( AdminItemImportForm, CampaignAdminForm, + CardAdminForm, + GuideAdminForm, ProjectAdminForm, SanitizedDescriptionAdminForm, - SimpleContentBlockAdminForm, ) logger = logging.getLogger(__name__) @@ -193,6 +198,7 @@ class CampaignAdmin(admin.ModelAdmin, CustomListDisplayFieldsMixin): ) list_display_links = ("title",) prepopulated_fields = {"slug": ("title",)} + raw_id_fields = ("card_family",) search_fields = ["title", "description"] list_filter = ( "published", @@ -793,19 +799,6 @@ def export_to_excel(self, request, queryset): actions = (export_to_csv, export_to_excel) -@admin.register(SimpleContentBlock) -class SimpleContentBlockAdmin(admin.ModelAdmin): - form = SimpleContentBlockAdminForm - - list_display = ("slug", "created_on", "updated_on") - readonly_fields = ("created_on", "updated_on") - - fieldsets = ( - (None, {"fields": ("created_on", "updated_on", "slug")}), - ("Body", {"classes": ("markdown-preview",), "fields": ("body",)}), - ) - - @admin.register(CarouselSlide) class CarouselSlideAdmin(admin.ModelAdmin): list_display = ("headline", "published", "ordering") @@ -991,3 +984,30 @@ def completion(self, obj): total = obj.project_total + obj.item_total + obj.asset_total removed = obj.projects_removed + obj.items_removed + obj.assets_removed return "{}%".format(round(removed / total * 100, 2)) + + +@admin.register(Card) +class CardAdmin(admin.ModelAdmin): + form = CardAdminForm + fields = ("title", "display_heading", "body_text", "image", "image_alt_text") + list_display = ["title", "created_on", "updated_on"] + list_filter = (CardCampaignListFilter, "updated_on") + + +class TutorialInline(admin.TabularInline): + model = TutorialCard + extra = 1 + raw_id_fields = ("card",) + + +@admin.register(CardFamily) +class CardFamilyAdmin(admin.ModelAdmin): + inlines = (TutorialInline,) + + class Media: + js = ("admin/custom-inline.js",) + + +@admin.register(Guide) +class GuideAdmin(admin.ModelAdmin): + form = GuideAdminForm diff --git a/concordia/admin/filters.py b/concordia/admin/filters.py index 5edad3b9b..8455f9423 100644 --- a/concordia/admin/filters.py +++ b/concordia/admin/filters.py @@ -67,6 +67,31 @@ def queryset(self, request, queryset): return queryset +class CardCampaignListFilter(admin.SimpleListFilter): + """ + Allow CMs to filter cards by campaign + """ + + title = _("campaign") + parameter_name = "campaign" + + def lookups(self, request, model_admin): + return Campaign.objects.exclude(card_family__isnull=True).values_list( + "pk", "title" + ) + + def queryset(self, request, queryset): + campaign_id = self.value() + if campaign_id: + card_family = Campaign.objects.get(pk=campaign_id).card_family + if card_family is None: + pks = [] + else: + pks = card_family.cards.values_list("pk", flat=True) + queryset = queryset.filter(id__in=pks) + return queryset + + class ProjectCampaignListFilter(CampaignListFilter): parameter_name = "campaign__id__exact" status_filter_parameter = "campaign__status" diff --git a/concordia/admin/forms.py b/concordia/admin/forms.py index d47ea8383..23c608968 100644 --- a/concordia/admin/forms.py +++ b/concordia/admin/forms.py @@ -2,7 +2,7 @@ from django import forms from tinymce.widgets import TinyMCE -from ..models import Campaign, Project +from ..models import Campaign, Card, Guide, Project FRAGMENT_ALLOWED_TAGS = { "a", @@ -89,7 +89,7 @@ def clean_short_description(self): ) -class CampaignAdminForm(forms.ModelForm): +class CampaignAdminForm(SanitizedDescriptionAdminForm): class Meta(SanitizedDescriptionAdminForm.Meta): model = Campaign widgets = { @@ -114,3 +114,21 @@ def clean_body(self): tags=BLOCK_ALLOWED_TAGS, attributes=ALLOWED_ATTRIBUTES, ) + + +class CardAdminForm(forms.ModelForm): + class Meta: + model = Card + widgets = { + "body_text": TinyMCE(), + } + fields = "__all__" + + +class GuideAdminForm(forms.ModelForm): + class Meta: + model = Guide + widgets = { + "body": TinyMCE(), + } + fields = "__all__" diff --git a/concordia/admin/views.py b/concordia/admin/views.py index bd15b06a9..dab1609f4 100644 --- a/concordia/admin/views.py +++ b/concordia/admin/views.py @@ -1,16 +1,20 @@ import re import tempfile import time +from http import HTTPStatus from bittersweet.models import validated_get_or_create from celery import Celery +from django.apps import apps from django.contrib import messages from django.contrib.admin.views.decorators import staff_member_required from django.contrib.auth.decorators import permission_required from django.core.exceptions import ValidationError from django.db.models import OuterRef, Subquery +from django.http import JsonResponse from django.shortcuts import render from django.utils.text import slugify +from django.views import View from django.views.decorators.cache import never_cache from tabular_export.core import export_to_csv_response, flatten_queryset @@ -611,3 +615,18 @@ def admin_retired_site_report_view(request): data.append(row) return export_to_csv_response("retired-site-report.csv", headers, data) + + +class SerializedObjectView(View): + def get(self, request, *args, **kwargs): + model_name = request.GET.get("model_name") + object_id = request.GET.get("object_id") + field_name = request.GET.get("field_name") + + model = apps.get_model(app_label="concordia", model_name=model_name) + try: + instance = model.objects.get(pk=object_id) + value = getattr(instance, field_name) + return JsonResponse({field_name: value}) + except model.DoesNotExist: + return JsonResponse({"status": "false"}, status=HTTPStatus.NOT_FOUND) diff --git a/concordia/admin_site.py b/concordia/admin_site.py index dbdf0831c..af00bf6e9 100644 --- a/concordia/admin_site.py +++ b/concordia/admin_site.py @@ -8,6 +8,7 @@ class ConcordiaAdminSite(admin.AdminSite): def get_urls(self): from concordia.admin.views import ( + SerializedObjectView, admin_bulk_import_review, admin_bulk_import_view, admin_retired_site_report_view, @@ -37,6 +38,11 @@ def get_urls(self): project_level_export, name="project-level-export", ), + path( + "serialized_object/", + SerializedObjectView.as_view(), + name="serialized_object", + ), ] return custom_urls + urls diff --git a/concordia/migrations/0087_auto_20240213_0756.py b/concordia/migrations/0087_auto_20240213_0756.py new file mode 100644 index 000000000..b17473a7f --- /dev/null +++ b/concordia/migrations/0087_auto_20240213_0756.py @@ -0,0 +1,136 @@ +# Generated by Django 3.2.23 on 2024-02-13 12:56 + +import django.db.models.deletion +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ("concordia", "0086_auto_20231215_1311"), + ] + + operations = [ + migrations.CreateModel( + name="Card", + fields=[ + ( + "id", + models.AutoField( + auto_created=True, + primary_key=True, + serialize=False, + verbose_name="ID", + ), + ), + ("image_alt_text", models.TextField(blank=True)), + ( + "image", + models.ImageField(blank=True, null=True, upload_to="card_images"), + ), + ("title", models.CharField(max_length=80)), + ("body_text", models.TextField(blank=True)), + ("created_on", models.DateTimeField(auto_now_add=True)), + ("updated_on", models.DateTimeField(auto_now=True, null=True)), + ( + "display_heading", + models.CharField(blank=True, max_length=80, null=True), + ), + ], + options={ + "ordering": ("title",), + }, + ), + migrations.CreateModel( + name="CardFamily", + fields=[ + ( + "id", + models.AutoField( + auto_created=True, + primary_key=True, + serialize=False, + verbose_name="ID", + ), + ), + ( + "slug", + models.SlugField(allow_unicode=True, max_length=80, unique=True), + ), + ("default", models.BooleanField(default=False)), + ], + options={ + "verbose_name_plural": "card families", + }, + ), + migrations.CreateModel( + name="Guide", + fields=[ + ( + "id", + models.AutoField( + auto_created=True, + primary_key=True, + serialize=False, + verbose_name="ID", + ), + ), + ("title", models.CharField(max_length=80)), + ("body", models.TextField(blank=True)), + ("order", models.IntegerField(default=1)), + ("link_text", models.CharField(blank=True, max_length=80, null=True)), + ("link_url", models.CharField(blank=True, max_length=255, null=True)), + ], + ), + migrations.CreateModel( + name="TutorialCard", + fields=[ + ( + "id", + models.AutoField( + auto_created=True, + primary_key=True, + serialize=False, + verbose_name="ID", + ), + ), + ("order", models.IntegerField(default=0)), + ( + "card", + models.ForeignKey( + on_delete=django.db.models.deletion.CASCADE, to="concordia.card" + ), + ), + ( + "tutorial", + models.ForeignKey( + on_delete=django.db.models.deletion.CASCADE, + to="concordia.cardfamily", + ), + ), + ], + options={ + "verbose_name_plural": "cards", + }, + ), + migrations.DeleteModel( + name="SimpleContentBlock", + ), + migrations.AddField( + model_name="cardfamily", + name="cards", + field=models.ManyToManyField( + through="concordia.TutorialCard", to="concordia.Card" + ), + ), + migrations.AddField( + model_name="campaign", + name="card_family", + field=models.ForeignKey( + blank=True, + null=True, + on_delete=django.db.models.deletion.CASCADE, + to="concordia.cardfamily", + ), + ), + ] diff --git a/concordia/models.py b/concordia/models.py index d119d6077..7f321c68a 100644 --- a/concordia/models.py +++ b/concordia/models.py @@ -190,6 +190,47 @@ def get_next_review_campaigns(self): return self.filter(next_review_campaign=True) +class Card(models.Model): + image_alt_text = models.TextField(blank=True) + image = models.ImageField(upload_to="card_images", blank=True, null=True) + title = models.CharField(max_length=80) + body_text = models.TextField(blank=True) + created_on = models.DateTimeField(editable=False, auto_now_add=True) + updated_on = models.DateTimeField(editable=False, auto_now=True, null=True) + display_heading = models.CharField(max_length=80, blank=True, null=True) + + def __str__(self): + return self.title + + class Meta: + ordering = ("title",) + + +class CardFamily(models.Model): + slug = models.SlugField(max_length=80, unique=True, allow_unicode=True) + default = models.BooleanField(default=False) + cards = models.ManyToManyField(Card, through="TutorialCard") + + class Meta: + verbose_name_plural = "card families" + + def __str__(self): + return self.slug + + +def on_cardfamily_save(sender, instance, **kwargs): + # Only one tutorial/ list of cards should be marked as "default". + # If the flag is set on a tutorial, it needs to be cleared from + # any other existing tutorials. + if instance.default: + CardFamily.objects.filter(default=True).exclude(pk=instance.pk).update( + default=False + ) + + +post_save.connect(on_cardfamily_save, sender=CardFamily) + + class Campaign(MetricsModelMixin("campaign"), models.Model): class Status(models.IntegerChoices): ACTIVE = 1 @@ -214,13 +255,17 @@ class Status(models.IntegerChoices): title = models.CharField(max_length=80) slug = models.SlugField(max_length=80, unique=True, allow_unicode=True) + card_family = models.ForeignKey( + CardFamily, on_delete=models.CASCADE, blank=True, null=True + ) + thumbnail_image = models.ImageField( + upload_to="campaign-thumbnails", blank=True, null=True + ) + launch_date = models.DateField(null=True, blank=True) completed_date = models.DateField(null=True, blank=True) description = models.TextField(blank=True) - thumbnail_image = models.ImageField( - upload_to="campaign-thumbnails", blank=True, null=True - ) short_description = models.TextField(blank=True) metadata = JSONField(default=metadata_default, blank=True, null=True) @@ -689,22 +734,6 @@ class AssetTranscriptionReservation(models.Model): tombstoned = models.BooleanField(default=False, blank=True, null=True) -class SimpleContentBlock(models.Model): - created_on = models.DateTimeField(editable=False, auto_now_add=True) - updated_on = models.DateTimeField(editable=False, auto_now=True) - - slug = models.SlugField( - unique=True, - max_length=255, - help_text="Label that templates use to retrieve this block", - ) - - body = models.TextField() - - def __str__(self): - return f"SimpleContentBlock: {self.slug}" - - class SimplePage(models.Model): created_on = models.DateTimeField(editable=False, auto_now_add=True) updated_on = models.DateTimeField(editable=False, auto_now=True) @@ -909,28 +938,21 @@ def __str__(self): return f"Removal progress for {self.campaign}" -class Card(models.Model): - image = models.ImageField(upload_to="card_images", blank=True, null=True) - title = models.CharField(max_length=80) - body_text = models.TextField(blank=True) - - class Meta: - abstract = True - - -class CardFamily(models.Model): - slug = models.SlugField(max_length=80, unique=True, allow_unicode=True) - default = models.BooleanField(default=False) - cards = models.ManyToManyField(Card, through="TutorialCard") - - class Meta: - abstract = True - - class TutorialCard(models.Model): card = models.ForeignKey(Card, on_delete=models.CASCADE) tutorial = models.ForeignKey(CardFamily, on_delete=models.CASCADE) order = models.IntegerField(default=0) class Meta: - abstract = True + verbose_name_plural = "cards" + + +class Guide(models.Model): + title = models.CharField(max_length=80) + body = models.TextField(blank=True) + order = models.IntegerField(default=1) + link_text = models.CharField(max_length=80, blank=True, null=True) + link_url = models.CharField(max_length=255, blank=True, null=True) + + def __str__(self): + return self.title diff --git a/concordia/settings_template.py b/concordia/settings_template.py index a6a83e739..bc9f0b5c0 100644 --- a/concordia/settings_template.py +++ b/concordia/settings_template.py @@ -369,7 +369,6 @@ # Feature flags FLAGS = { "ADVERTISE_ACTIVITY_UI": [], - "SIMPLE_CONTENT_BLOCKS": [], "CAROUSEL_CMS": [], "SEND_WELCOME_EMAIL": [], "SHOW_BANNER": [], @@ -397,7 +396,8 @@ "skin": "oxide-dark", "content_css": "dark", "plugins": "link lists searchreplace wordcount", - "toolbar1": "bold italic | numlist bullist | link | hr | searchreplace wordcount", + "browser_spellcheck": "true", + "toolbar1": "bold italic | numlist bullist | link | searchreplace wordcount", } TINYMCE_JS_URL = "https://cdn.tiny.cloud/1/rf486i5f1ww9m8191oolczn7f0ry61mzdtfwbu7maiiiv2kv/tinymce/6/tinymce.min.js" diff --git a/concordia/static/admin/custom-inline.js b/concordia/static/admin/custom-inline.js new file mode 100644 index 000000000..2686c3a2e --- /dev/null +++ b/concordia/static/admin/custom-inline.js @@ -0,0 +1,41 @@ +/* global jQuery */ + +(function ($) { + function triggerChangeOnField(win, chosenId) { + var element = document.getElementById(win.name); + + $.ajax({ + url: '/admin/serialized_object/', + data: { + model_name: 'Card', + object_id: chosenId, + field_name: 'title', + }, + dataType: 'json', + success: function (data) { + const newContent = document.createTextNode(data.title); + var a = document.createElement('a'); + a.href = '/admin/card/' + chosenId + '/change/'; + a.append(newContent); + var newStrong = document.createElement('strong'); + newStrong.append(a); + var strong = element.parentNode.querySelector('strong'); + if (strong) { + strong.replaceWith(newStrong); + } else { + element.parentNode.append(newStrong); + } + }, + }); + } + + $(document).ready(function () { + // https://stackoverflow.com/a/33937138/10320488 + window.ORIGINAL_dismissRelatedLookupPopup = + window.dismissRelatedLookupPopup; + window.dismissRelatedLookupPopup = function (win, chosenId) { + window.ORIGINAL_dismissRelatedLookupPopup(win, chosenId); + triggerChangeOnField(win, chosenId); + }; + }); +})(jQuery); diff --git a/concordia/static/scss/base.scss b/concordia/static/scss/base.scss index 36ddf2177..9cb364fd8 100644 --- a/concordia/static/scss/base.scss +++ b/concordia/static/scss/base.scss @@ -271,6 +271,10 @@ ul.nav-secondary { flex: initial !important; } +#transcription-input { + width: 99% !important; +} + /* * vertical and horizontal dividers */ @@ -843,33 +847,6 @@ $card-progress-height: 12px; color: #242424; } -/* End of help center stuff */ - -#instruction-window { - position: absolute; - bottom: 0; - right: 0; - left: 0; - z-index: 10; - /* stylelint-disable-next-line color-function-notation */ - background-color: rgba(250, 250, 250, 80%); - border-top: 1pt solid $dark; - transition: height 1s linear 0s; -} - -#instruction-window.collapse { - display: none; -} - -#instruction-window.collapse.show { - display: block; -} - -#instruction-window p { - max-width: 40em; - margin: auto; -} - /* * Image filter controls */ @@ -1039,6 +1016,129 @@ $card-progress-height: 12px; } } +/* + * Tutorial popup and cards navigation + */ +#tutorial-popup .modal-header { + padding-bottom: 0.25rem; +} + +#tutorial-popup .modal-body { + padding-top: 0.25rem; +} + +#tutorial-popup .close { + position: absolute; + right: 1rem; + top: 0.75rem; +} + +#card-carousel .carousel-item img { + background-color: #fff; + border-top: 1px solid #efefef; + padding-bottom: 1rem; +} + +#card-carousel .carousel-item h5 { + margin-bottom: 0; +} + +#card-carousel .carousel-item p { + margin-bottom: 0.75rem; +} + +#card-carousel ul { + padding-left: 1.5rem; +} + +#card-carousel .carousel-indicators { + left: 50%; + width: 60%; + margin-left: -30%; + text-align: center; +} + +#card-carousel .carousel-indicators > li.active { + background-color: #007bff; + border-color: #007bff; +} + +#previous-card { + position: absolute; + bottom: 10px; + left: 0; +} + +#next-card { + position: absolute; + bottom: 10px; + right: 0; +} + +/* How to Guide */ +#open-guide { + transform: rotate(-90deg) translateY(-50%); + position: absolute; + border-radius: 0; + right: 0; + top: 129px; + font-size: 0.75rem; + margin-right: -55px; + margin-top: 39px; + width: 110px; +} + +#close-guide { + height: 30.5px; + margin: 0 1rem -1rem auto; +} + +.sidebar { + height: 580px; /* 100% Full-height */ + width: 0; /* 0 width - change this with JavaScript */ + position: absolute; /* Stay in place */ + z-index: 1; /* Stay on top */ + top: 129px; + right: 0; + overflow-x: hidden; /* Disable horizontal scroll */ + transition: 0.5s; /* 0.5 second transition effect to slide in the sidebar */ + background-color: $white; +} + +.sidebar h3 { + padding-top: 0.5rem; +} + +.sidebar li { + border-bottom: thin solid $gray-400; +} + +.sidebar .nav-item a { + font-size: $font-size-base * 1.25; + text-decoration: underline; +} + +.guide-body h3 { + font-size: 1rem; + font-weight: bold; +} + +#title-bar { + font-weight: 700; +} + +.toc-title { + font-weight: 600; +} + +#guide-bars { + color: #fff; +} + +.sidebar .close { + font-size: $font-size-base * 0.875; +} + /* * Campaign Resources Panel */ diff --git a/concordia/templates/admin/concordia/simplecontentblock/change_form.html b/concordia/templates/admin/concordia/simplecontentblock/change_form.html deleted file mode 100644 index b6b130a3b..000000000 --- a/concordia/templates/admin/concordia/simplecontentblock/change_form.html +++ /dev/null @@ -1,15 +0,0 @@ -{% extends "admin/change_form.html" %} - -{% block extrahead %} - {{ block.super }} - - {% include 'fragments/codemirror.html' %} -{% endblock extrahead %} - -{% block content %} - {{ block.super }} - - -{% endblock content %} diff --git a/concordia/templates/base.html b/concordia/templates/base.html index b60b34857..2e8bae2bc 100644 --- a/concordia/templates/base.html +++ b/concordia/templates/base.html @@ -290,5 +290,34 @@