diff --git a/.circleci/config.yml b/.circleci/config.yml index 90e62ff4..706e8e41 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -24,7 +24,10 @@ jobs: name: Set up virtual environment command: | pipenv sync --dev - - run: pipenv run playwright install --with-deps chromium + + - run: pipenv run ruff check . + - run: ./scripts/lint.sh + - run: ./scripts/format.sh - run: make lambda-layers/DependenciesLayer/requirements.txt - run: npm install - run: npm run build diff --git a/Pipfile b/Pipfile index 2627f521..b6f14d8b 100644 --- a/Pipfile +++ b/Pipfile @@ -21,7 +21,6 @@ djangorestframework = ">=3.9.1" durationpy = "==0.4" enum34 = "==1.1.6" fixtures = "==3.0.0" -flake8 = "==3.8.3" funcsigs = "==1.0.2" dj-pagination = "==2.4.0" linecache2 = "==1.0.0" @@ -64,7 +63,6 @@ django-static-jquery = "==2.1.4" setuptools = "*" faker = "*" werkzeug = "*" -black = "==19.10b0" django-s3file = "*" psycopg2-binary = "*" dc_design_system = {version = "0.5.0", git = "https://github.com/DemocracyClub/design-system.git" } @@ -85,6 +83,7 @@ pytest = "*" pytest-playwright = "*" pytest-cov = "*" moto = "*" +ruff = "*" [requires] python_version = "3.12" diff --git a/Pipfile.lock b/Pipfile.lock index 97dfb79a..9376c59b 100644 --- a/Pipfile.lock +++ b/Pipfile.lock @@ -1,7 +1,7 @@ { "_meta": { "hash": { - "sha256": "8b44b80f3d9ce26cbcc432466075e098e5e6fc1dadb997923c810ddf5078862a" + "sha256": "4853d724475f2f6adb46510bc4dc7dbede2c79eb783762b621447dc3b9ffb88a" }, "pipfile-spec": 6, "requires": { @@ -36,13 +36,6 @@ ], "version": "==1.1.8" }, - "appdirs": { - "hashes": [ - "sha256:7d5d0167b2b1ba821647616af46a749d1c653740dd0d2415100fe26e27afdf41", - "sha256:a841dacd6b99318a741b166adb07e19ee71a274450e68237b4650ca1055ab128" - ], - "version": "==1.4.4" - }, "argparse": { "hashes": [ "sha256:62b089a55be1d8949cd2bc7e0df0bddb9e028faefc8c32038cc84862aefdd6e4", @@ -58,27 +51,10 @@ "markers": "python_version >= '3.8'", "version": "==3.8.1" }, - "attrs": { - "hashes": [ - "sha256:5cfb1b9148b5b086569baec03f20d7b6bf3bcacc9a42bebf87ffaaca362f6346", - "sha256:81921eb96de3191c8258c199618104dd27ac608d9366f5e35d011eae1867ede2" - ], - "markers": "python_version >= '3.7'", - "version": "==24.2.0" - }, "aws-wsgi": { "git": "https://github.com/DemocracyClub/awsgi.git", "ref": "a5b800373c326e75f705d400c48f7df301409724" }, - "black": { - "hashes": [ - "sha256:1b30e59be925fafc1ee4565e5e08abef6b03fe455102883820fe5ee2e4734e0b", - "sha256:c2edb73a08e9e0e6f65a0e6af18b059b8b1cdd5bef997d7a0b181df93dc81539" - ], - "index": "pypi", - "markers": "python_version >= '3.6'", - "version": "==19.10b0" - }, "blinker": { "hashes": [ "sha256:b4ce2265a7abece45e7cc896e98dbebe6cead56bcf805a3d23136d145f5445bf", @@ -89,20 +65,20 @@ }, "boto3": { "hashes": [ - "sha256:091d6bed1422370987a839bff3f8755df7404fc15e9fac2a48e8505356f07433", - "sha256:9b26fa31901da7793c1dcd65eee9bab7e897d8aa1ffed0b5e1c3bce93d2aefe4" + "sha256:3ed7172b3d4fceb6218bb0ec3668c4d40c03690939c2fca4f22bb875d741a07f", + "sha256:e2969a246bb3208122b3c349c49cc6604c6fc3fc2b2f65d99d3e8ccd745b0c16" ], "index": "pypi", "markers": "python_version >= '3.8'", - "version": "==1.35.68" + "version": "==1.35.71" }, "botocore": { "hashes": [ - "sha256:42c3700583a82f2b5316281a073d644a521d6358837e2b446dc458ba5d990fb4", - "sha256:599139d5564291f5be873800711f9e4e14a823395ae9ce7b142be775e9849b94" + "sha256:f9fa058e0393660c3fe53c1e044751beb64b586def0bd2212448a7c328b0cbba", + "sha256:fc46e7ab1df3cef66dfba1633f4da77c75e07365b36f03bd64a3793634be8fc1" ], "markers": "python_version >= '3.8'", - "version": "==1.35.68" + "version": "==1.35.71" }, "certifi": { "hashes": [ @@ -329,105 +305,111 @@ "index": "pypi", "version": "==0.5.5" }, - "couleur": { - "hashes": [ - "sha256:1e96a5972ecd5f88716fe4e4df63d31e85f57d3ff6c02a07e93a226a663961ba" - ], - "version": "==0.7.4" - }, "coverage": { "extras": [ "toml" ], "hashes": [ - "sha256:04387a4a6ecb330c1878907ce0dc04078ea72a869263e53c72a1ba5bbdf380ca", - "sha256:0676cd0ba581e514b7f726495ea75aba3eb20899d824636c6f59b0ed2f88c471", - "sha256:0e8d06778e8fbffccfe96331a3946237f87b1e1d359d7fbe8b06b96c95a5407a", - "sha256:0eb3c2f32dabe3a4aaf6441dde94f35687224dfd7eb2a7f47f3fd9428e421058", - "sha256:109f5985182b6b81fe33323ab4707011875198c41964f014579cf82cebf2bb85", - "sha256:13eaf476ec3e883fe3e5fe3707caeb88268a06284484a3daf8250259ef1ba143", - "sha256:164fdcc3246c69a6526a59b744b62e303039a81e42cfbbdc171c91a8cc2f9446", - "sha256:26776ff6c711d9d835557ee453082025d871e30b3fd6c27fcef14733f67f0590", - "sha256:26f66da8695719ccf90e794ed567a1549bb2644a706b41e9f6eae6816b398c4a", - "sha256:29f3abe810930311c0b5d1a7140f6395369c3db1be68345638c33eec07535105", - "sha256:316543f71025a6565677d84bc4df2114e9b6a615aa39fb165d697dba06a54af9", - "sha256:36b0ea8ab20d6a7564e89cb6135920bc9188fb5f1f7152e94e8300b7b189441a", - "sha256:3cc9d4bc55de8003663ec94c2f215d12d42ceea128da8f0f4036235a119c88ac", - "sha256:485e9f897cf4856a65a57c7f6ea3dc0d4e6c076c87311d4bc003f82cfe199d25", - "sha256:5040148f4ec43644702e7b16ca864c5314ccb8ee0751ef617d49aa0e2d6bf4f2", - "sha256:51456e6fa099a8d9d91497202d9563a320513fcf59f33991b0661a4a6f2ad450", - "sha256:53d7d9158ee03956e0eadac38dfa1ec8068431ef8058fe6447043db1fb40d932", - "sha256:5a10a4920def78bbfff4eff8a05c51be03e42f1c3735be42d851f199144897ba", - "sha256:5b14b4f8760006bfdb6e08667af7bc2d8d9bfdb648351915315ea17645347137", - "sha256:5b2ccb7548a0b65974860a78c9ffe1173cfb5877460e5a229238d985565574ae", - "sha256:697d1317e5290a313ef0d369650cfee1a114abb6021fa239ca12b4849ebbd614", - "sha256:6ae8c9d301207e6856865867d762a4b6fd379c714fcc0607a84b92ee63feff70", - "sha256:707c0f58cb1712b8809ece32b68996ee1e609f71bd14615bd8f87a1293cb610e", - "sha256:74775198b702868ec2d058cb92720a3c5a9177296f75bd97317c787daf711505", - "sha256:756ded44f47f330666843b5781be126ab57bb57c22adbb07d83f6b519783b870", - "sha256:76f03940f9973bfaee8cfba70ac991825611b9aac047e5c80d499a44079ec0bc", - "sha256:79287fd95585ed36e83182794a57a46aeae0b64ca53929d1176db56aacc83451", - "sha256:799c8f873794a08cdf216aa5d0531c6a3747793b70c53f70e98259720a6fe2d7", - "sha256:7d360587e64d006402b7116623cebf9d48893329ef035278969fa3bbf75b697e", - "sha256:80b5ee39b7f0131ebec7968baa9b2309eddb35b8403d1869e08f024efd883566", - "sha256:815ac2d0f3398a14286dc2cea223a6f338109f9ecf39a71160cd1628786bc6f5", - "sha256:83c2dda2666fe32332f8e87481eed056c8b4d163fe18ecc690b02802d36a4d26", - "sha256:846f52f46e212affb5bcf131c952fb4075b55aae6b61adc9856222df89cbe3e2", - "sha256:936d38794044b26c99d3dd004d8af0035ac535b92090f7f2bb5aa9c8e2f5cd42", - "sha256:9864463c1c2f9cb3b5db2cf1ff475eed2f0b4285c2aaf4d357b69959941aa555", - "sha256:995ea5c48c4ebfd898eacb098164b3cc826ba273b3049e4a889658548e321b43", - "sha256:a1526d265743fb49363974b7aa8d5899ff64ee07df47dd8d3e37dcc0818f09ed", - "sha256:a56de34db7b7ff77056a37aedded01b2b98b508227d2d0979d373a9b5d353daa", - "sha256:a7c97726520f784239f6c62506bc70e48d01ae71e9da128259d61ca5e9788516", - "sha256:b8e99f06160602bc64da35158bb76c73522a4010f0649be44a4e167ff8555952", - "sha256:bb1de682da0b824411e00a0d4da5a784ec6496b6850fdf8c865c1d68c0e318dd", - "sha256:bf477c355274a72435ceb140dc42de0dc1e1e0bf6e97195be30487d8eaaf1a09", - "sha256:bf635a52fc1ea401baf88843ae8708591aa4adff875e5c23220de43b1ccf575c", - "sha256:bfd5db349d15c08311702611f3dccbef4b4e2ec148fcc636cf8739519b4a5c0f", - "sha256:c530833afc4707fe48524a44844493f36d8727f04dcce91fb978c414a8556cc6", - "sha256:cc6d65b21c219ec2072c1293c505cf36e4e913a3f936d80028993dd73c7906b1", - "sha256:cd3c1e4cb2ff0083758f09be0f77402e1bdf704adb7f89108007300a6da587d0", - "sha256:cfd2a8b6b0d8e66e944d47cdec2f47c48fef2ba2f2dff5a9a75757f64172857e", - "sha256:d0ca5c71a5a1765a0f8f88022c52b6b8be740e512980362f7fdbb03725a0d6b9", - "sha256:e7defbb9737274023e2d7af02cac77043c86ce88a907c58f42b580a97d5bcca9", - "sha256:e9d1bf53c4c8de58d22e0e956a79a5b37f754ed1ffdbf1a260d9dcfa2d8a325e", - "sha256:ea81d8f9691bb53f4fb4db603203029643caffc82bf998ab5b59ca05560f4c06" + "sha256:093896e530c38c8e9c996901858ac63f3d4171268db2c9c8b373a228f459bbc5", + "sha256:09b9f848b28081e7b975a3626e9081574a7b9196cde26604540582da60235fdf", + "sha256:0b0c69f4f724c64dfbfe79f5dfb503b42fe6127b8d479b2677f2b227478db2eb", + "sha256:13618bed0c38acc418896005732e565b317aa9e98d855a0e9f211a7ffc2d6638", + "sha256:13690e923a3932e4fad4c0ebfb9cb5988e03d9dcb4c5150b5fcbf58fd8bddfc4", + "sha256:177f01eeaa3aee4a5ffb0d1439c5952b53d5010f86e9d2667963e632e30082cc", + "sha256:193e3bffca48ad74b8c764fb4492dd875038a2f9925530cb094db92bb5e47bed", + "sha256:1defe91d41ce1bd44b40fabf071e6a01a5aa14de4a31b986aa9dfd1b3e3e414a", + "sha256:1f188a2402f8359cf0c4b1fe89eea40dc13b52e7b4fd4812450da9fcd210181d", + "sha256:202a2d645c5a46b84992f55b0a3affe4f0ba6b4c611abec32ee88358db4bb649", + "sha256:24eda3a24a38157eee639ca9afe45eefa8d2420d49468819ac5f88b10de84f4c", + "sha256:2e4e0f60cb4bd7396108823548e82fdab72d4d8a65e58e2c19bbbc2f1e2bfa4b", + "sha256:379c111d3558272a2cae3d8e57e6b6e6f4fe652905692d54bad5ea0ca37c5ad4", + "sha256:37cda8712145917105e07aab96388ae76e787270ec04bcb9d5cc786d7cbb8443", + "sha256:38c51297b35b3ed91670e1e4efb702b790002e3245a28c76e627478aa3c10d83", + "sha256:3985b9be361d8fb6b2d1adc9924d01dec575a1d7453a14cccd73225cb79243ee", + "sha256:3988665ee376abce49613701336544041f2117de7b7fbfe91b93d8ff8b151c8e", + "sha256:3ac47fa29d8d41059ea3df65bd3ade92f97ee4910ed638e87075b8e8ce69599e", + "sha256:3b4b4299dd0d2c67caaaf286d58aef5e75b125b95615dda4542561a5a566a1e3", + "sha256:3ea8bb1ab9558374c0ab591783808511d135a833c3ca64a18ec927f20c4030f0", + "sha256:3fe47da3e4fda5f1abb5709c156eca207eacf8007304ce3019eb001e7a7204cb", + "sha256:428ac484592f780e8cd7b6b14eb568f7c85460c92e2a37cb0c0e5186e1a0d076", + "sha256:44e6c85bbdc809383b509d732b06419fb4544dca29ebe18480379633623baafb", + "sha256:4674f0daa1823c295845b6a740d98a840d7a1c11df00d1fd62614545c1583787", + "sha256:4be32da0c3827ac9132bb488d331cb32e8d9638dd41a0557c5569d57cf22c9c1", + "sha256:4db3ed6a907b555e57cc2e6f14dc3a4c2458cdad8919e40b5357ab9b6db6c43e", + "sha256:5c52a036535d12590c32c49209e79cabaad9f9ad8aa4cbd875b68c4d67a9cbce", + "sha256:629a1ba2115dce8bf75a5cce9f2486ae483cb89c0145795603d6554bdc83e801", + "sha256:62a66ff235e4c2e37ed3b6104d8b478d767ff73838d1222132a7a026aa548764", + "sha256:63068a11171e4276f6ece913bde059e77c713b48c3a848814a6537f35afb8365", + "sha256:63c19702db10ad79151a059d2d6336fe0c470f2e18d0d4d1a57f7f9713875dcf", + "sha256:644ec81edec0f4ad17d51c838a7d01e42811054543b76d4ba2c5d6af741ce2a6", + "sha256:6535d996f6537ecb298b4e287a855f37deaf64ff007162ec0afb9ab8ba3b8b71", + "sha256:6f4548c5ead23ad13fb7a2c8ea541357474ec13c2b736feb02e19a3085fac002", + "sha256:716a78a342679cd1177bc8c2fe957e0ab91405bd43a17094324845200b2fddf4", + "sha256:74610105ebd6f33d7c10f8907afed696e79c59e3043c5f20eaa3a46fddf33b4c", + "sha256:768939f7c4353c0fac2f7c37897e10b1414b571fd85dd9fc49e6a87e37a2e0d8", + "sha256:86cffe9c6dfcfe22e28027069725c7f57f4b868a3f86e81d1c62462764dc46d4", + "sha256:8aae5aea53cbfe024919715eca696b1a3201886ce83790537d1c3668459c7146", + "sha256:8b2b8503edb06822c86d82fa64a4a5cb0760bb8f31f26e138ec743f422f37cfc", + "sha256:912e95017ff51dc3d7b6e2be158dedc889d9a5cc3382445589ce554f1a34c0ea", + "sha256:9a7b8ac36fd688c8361cbc7bf1cb5866977ece6e0b17c34aa0df58bda4fa18a4", + "sha256:9e89d5c8509fbd6c03d0dd1972925b22f50db0792ce06324ba069f10787429ad", + "sha256:ae270e79f7e169ccfe23284ff5ea2d52a6f401dc01b337efb54b3783e2ce3f28", + "sha256:b07c25d52b1c16ce5de088046cd2432b30f9ad5e224ff17c8f496d9cb7d1d451", + "sha256:b39e6011cd06822eb964d038d5dff5da5d98652b81f5ecd439277b32361a3a50", + "sha256:bd55f8fc8fa494958772a2a7302b0354ab16e0b9272b3c3d83cdb5bec5bd1779", + "sha256:c15b32a7aca8038ed7644f854bf17b663bc38e1671b5d6f43f9a2b2bd0c46f63", + "sha256:c1b4474beee02ede1eef86c25ad4600a424fe36cff01a6103cb4533c6bf0169e", + "sha256:c79c0685f142ca53256722a384540832420dff4ab15fec1863d7e5bc8691bdcc", + "sha256:c9ebfb2507751f7196995142f057d1324afdab56db1d9743aab7f50289abd022", + "sha256:d7ad66e8e50225ebf4236368cc43c37f59d5e6728f15f6e258c8639fa0dd8e6d", + "sha256:d82ab6816c3277dc962cfcdc85b1efa0e5f50fb2c449432deaf2398a2928ab94", + "sha256:d9fd2547e6decdbf985d579cf3fc78e4c1d662b9b0ff7cc7862baaab71c9cc5b", + "sha256:de38add67a0af869b0d79c525d3e4588ac1ffa92f39116dbe0ed9753f26eba7d", + "sha256:e19122296822deafce89a0c5e8685704c067ae65d45e79718c92df7b3ec3d331", + "sha256:e44961e36cb13c495806d4cac67640ac2866cb99044e210895b506c26ee63d3a", + "sha256:e4c81ed2820b9023a9a90717020315e63b17b18c274a332e3b6437d7ff70abe0", + "sha256:e683e6ecc587643f8cde8f5da6768e9d165cd31edf39ee90ed7034f9ca0eefee", + "sha256:f39e2f3530ed1626c66e7493be7a8423b023ca852aacdc91fb30162c350d2a92", + "sha256:f56f49b2553d7dd85fd86e029515a221e5c1f8cb3d9c38b470bc38bde7b8445a", + "sha256:fb9fc32399dca861584d96eccd6c980b69bbcd7c228d06fb74fe53e007aa8ef9" ], - "markers": "python_version >= '3.8'", - "version": "==7.4.0" + "markers": "python_version >= '3.9'", + "version": "==7.6.8" }, "cryptography": { "hashes": [ - "sha256:0c580952eef9bf68c4747774cde7ec1d85a6e61de97281f2dba83c7d2c806362", - "sha256:0f996e7268af62598f2fc1204afa98a3b5712313a55c4c9d434aef49cadc91d4", - "sha256:1ec0bcf7e17c0c5669d881b1cd38c4972fade441b27bda1051665faaa89bdcaa", - "sha256:281c945d0e28c92ca5e5930664c1cefd85efe80e5c0d2bc58dd63383fda29f83", - "sha256:2ce6fae5bdad59577b44e4dfed356944fbf1d925269114c28be377692643b4ff", - "sha256:315b9001266a492a6ff443b61238f956b214dbec9910a081ba5b6646a055a805", - "sha256:443c4a81bb10daed9a8f334365fe52542771f25aedaf889fd323a853ce7377d6", - "sha256:4a02ded6cd4f0a5562a8887df8b3bd14e822a90f97ac5e544c162899bc467664", - "sha256:53a583b6637ab4c4e3591a15bc9db855b8d9dee9a669b550f311480acab6eb08", - "sha256:63efa177ff54aec6e1c0aefaa1a241232dcd37413835a9b674b6e3f0ae2bfd3e", - "sha256:74f57f24754fe349223792466a709f8e0c093205ff0dca557af51072ff47ab18", - "sha256:7e1ce50266f4f70bf41a2c6dc4358afadae90e2a1e5342d3c08883df1675374f", - "sha256:81ef806b1fef6b06dcebad789f988d3b37ccaee225695cf3e07648eee0fc6b73", - "sha256:846da004a5804145a5f441b8530b4bf35afbf7da70f82409f151695b127213d5", - "sha256:8ac43ae87929a5982f5948ceda07001ee5e83227fd69cf55b109144938d96984", - "sha256:9762ea51a8fc2a88b70cf2995e5675b38d93bf36bd67d91721c309df184f49bd", - "sha256:a2a431ee15799d6db9fe80c82b055bae5a752bef645bba795e8e52687c69efe3", - "sha256:bf7a1932ac4176486eab36a19ed4c0492da5d97123f1406cf15e41b05e787d2e", - "sha256:c2e6fc39c4ab499049df3bdf567f768a723a5e8464816e8f009f121a5a9f4405", - "sha256:cbeb489927bd7af4aa98d4b261af9a5bc025bd87f0e3547e11584be9e9427be2", - "sha256:d03b5621a135bffecad2c73e9f4deb1a0f977b9a8ffe6f8e002bf6c9d07b918c", - "sha256:d56e96520b1020449bbace2b78b603442e7e378a9b3bd68de65c782db1507995", - "sha256:df6b6c6d742395dd77a23ea3728ab62f98379eff8fb61be2744d4679ab678f73", - "sha256:e1be4655c7ef6e1bbe6b5d0403526601323420bcf414598955968c9ef3eb7d16", - "sha256:f18c716be16bc1fea8e95def49edf46b82fccaa88587a45f8dc0ff6ab5d8e0a7", - "sha256:f46304d6f0c6ab8e52770addfa2fc41e6629495548862279641972b6215451cd", - "sha256:f7b178f11ed3664fd0e995a47ed2b5ff0a12d893e41dd0494f406d1cf555cab7" - ], - "markers": "python_version >= '3.7'", - "version": "==43.0.3" + "sha256:1923cb251c04be85eec9fda837661c67c1049063305d6be5721643c22dd4e2b7", + "sha256:37d76e6863da3774cd9db5b409a9ecfd2c71c981c38788d3fcfaf177f447b731", + "sha256:3c672a53c0fb4725a29c303be906d3c1fa99c32f58abe008a82705f9ee96f40b", + "sha256:404fdc66ee5f83a1388be54300ae978b2efd538018de18556dde92575e05defc", + "sha256:4ac4c9f37eba52cb6fbeaf5b59c152ea976726b865bd4cf87883a7e7006cc543", + "sha256:60eb32934076fa07e4316b7b2742fa52cbb190b42c2df2863dbc4230a0a9b385", + "sha256:62901fb618f74d7d81bf408c8719e9ec14d863086efe4185afd07c352aee1d2c", + "sha256:660cb7312a08bc38be15b696462fa7cc7cd85c3ed9c576e81f4dc4d8b2b31591", + "sha256:708ee5f1bafe76d041b53a4f95eb28cdeb8d18da17e597d46d7833ee59b97ede", + "sha256:761817a3377ef15ac23cd7834715081791d4ec77f9297ee694ca1ee9c2c7e5eb", + "sha256:831c3c4d0774e488fdc83a1923b49b9957d33287de923d58ebd3cec47a0ae43f", + "sha256:84111ad4ff3f6253820e6d3e58be2cc2a00adb29335d4cacb5ab4d4d34f2a123", + "sha256:8b3e6eae66cf54701ee7d9c83c30ac0a1e3fa17be486033000f2a73a12ab507c", + "sha256:9abcc2e083cbe8dde89124a47e5e53ec38751f0d7dfd36801008f316a127d7ba", + "sha256:9e6fc8a08e116fb7c7dd1f040074c9d7b51d74a8ea40d4df2fc7aa08b76b9e6c", + "sha256:a01956ddfa0a6790d594f5b34fc1bfa6098aca434696a03cfdbe469b8ed79285", + "sha256:abc998e0c0eee3c8a1904221d3f67dcfa76422b23620173e28c11d3e626c21bd", + "sha256:b15492a11f9e1b62ba9d73c210e2416724633167de94607ec6069ef724fad092", + "sha256:be4ce505894d15d5c5037167ffb7f0ae90b7be6f2a98f9a5c3442395501c32fa", + "sha256:c5eb858beed7835e5ad1faba59e865109f3e52b3783b9ac21e7e47dc5554e289", + "sha256:cd4e834f340b4293430701e772ec543b0fbe6c2dea510a5286fe0acabe153a02", + "sha256:d2436114e46b36d00f8b72ff57e598978b37399d2786fd39793c36c6d5cb1c64", + "sha256:eb33480f1bad5b78233b0ad3e1b0be21e8ef1da745d8d2aecbb20671658b9053", + "sha256:eca27345e1214d1b9f9490d200f9db5a874479be914199194e746c893788d417", + "sha256:ed3534eb1090483c96178fcb0f8893719d96d5274dfde98aa6add34614e97c8e", + "sha256:f3f6fdfa89ee2d9d496e2c087cebef9d4fcbb0ad63c40e821b39f74bf48d9c5e", + "sha256:f53c2c87e0fb4b0c00fa9571082a057e37690a8f12233306161c8f4b819960b7", + "sha256:f5e7cb1e5e56ca0933b4873c0220a78b773b24d40d186b6738080b73d3d0a756", + "sha256:f677e1268c4e23420c3acade68fac427fffcb8d19d7df95ed7ad17cdef8404f4" + ], + "markers": "python_version >= '3.7' and python_full_version not in '3.9.0, 3.9.1'", + "version": "==44.0.0" }, "cssselect": { "hashes": [ @@ -537,12 +519,12 @@ }, "django-ses": { "hashes": [ - "sha256:2344f2650b699417c0f2b6197800580f5022aaccf0083c2a1b640b7ff7366067", - "sha256:6167401bd3fc2b29a33b35e4f6f124dc738f6612e3a926ff5533143e75c71ff1" + "sha256:3f2374368251c3d614d6409d7f477747ebba705df38823eb3bf3b578c916f9c6", + "sha256:91ed468ae1d64bbbebb24661c9e324f49e52de4d228744d1d5ace203ffd4e4e6" ], "index": "pypi", "markers": "python_version >= '3.8' and python_version < '4.0'", - "version": "==4.2.0" + "version": "==4.3.0" }, "django-static-jquery": { "hashes": [ @@ -617,12 +599,12 @@ }, "faker": { "hashes": [ - "sha256:68e5580cb6b4226710886e595eabc13127149d6e71e9d1db65506a7fbe2c7fce", - "sha256:9b01019c1ddaf2253ca2308c0472116e993f4ad8fc9905f82fa965e0c6f932e9" + "sha256:1c925fc0e86a51fc46648b504078c88d0cd48da1da2595c4e712841cab43a1e4", + "sha256:d30c5f0e2796b8970de68978365247657486eb0311c5abe88d0b895b68dff05d" ], "index": "pypi", "markers": "python_version >= '3.8'", - "version": "==33.0.0" + "version": "==33.1.0" }, "fixtures": { "hashes": [ @@ -632,15 +614,6 @@ "index": "pypi", "version": "==3.0.0" }, - "flake8": { - "hashes": [ - "sha256:15e351d19611c887e482fb960eae4d44845013cc142d42896e9862f775d8cf5c", - "sha256:f04b9fcbac03b0a3e58c0ab3a0ecc462e023a9faf046d57794184028123aa208" - ], - "index": "pypi", - "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'", - "version": "==3.8.3" - }, "flask": { "hashes": [ "sha256:77fd4e1249d8c9923de34907236b747ced06e5467ecac1a7bb7115ae0e9670b0", @@ -1181,14 +1154,6 @@ "markers": "python_version >= '3.8'", "version": "==24.2" }, - "pathspec": { - "hashes": [ - "sha256:a0d503e138a4c123b27490a4f7beda6a01c6f288df0e4a8b79c7eb0dc7b4cc08", - "sha256:a482d51503a1ab33b1c67a6c3813a26953dbdc71c31dacaef9a838c4e29f5712" - ], - "markers": "python_version >= '3.8'", - "version": "==0.12.1" - }, "pbr": { "hashes": [ "sha256:568f988af109114fbfa0525dcb6836b069838360d11732736ecc82e4c15d5c12", @@ -1395,14 +1360,6 @@ "markers": "python_version >= '3.8'", "version": "==2.9.10" }, - "pycodestyle": { - "hashes": [ - "sha256:2295e7b2f6b5bd100585ebcb1f616591b652db8a741695b3d8f5d28bdc934367", - "sha256:c58a7d2815e0e8d7972bf1803331fb0152f867bd89adf8a01dfd55085434192e" - ], - "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'", - "version": "==2.6.0" - }, "pycparser": { "hashes": [ "sha256:491c8be9c040f5390f5bf44a5b07752bd07f56edf992381b05c701439eec10f6", @@ -1419,14 +1376,6 @@ "markers": "python_version >= '3.8'", "version": "==12.0.0" }, - "pyflakes": { - "hashes": [ - "sha256:0d94e0e05a19e57a99444b6ddcf9a6eb2e5c68d3ca1e98e90707af8152c90a92", - "sha256:35b2d75ee967ea93b55750aa9edbbf72813e06a66ba54438df2cfac9e3c27fc8" - ], - "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'", - "version": "==2.2.0" - }, "pyparsing": { "hashes": [ "sha256:0832bcf47acd283788593e7a0f542407bd9550a55a8a8435214a1960e04bcb04", @@ -1466,12 +1415,12 @@ }, "pytest-cov": { "hashes": [ - "sha256:4f0764a1219df53214206bf1feea4633c3b558a2925c8b59f144f682861ce652", - "sha256:5837b58e9f6ebd335b0f8060eecce69b662415b16dc503883a02f45dfeb14857" + "sha256:eee6f1b9e61008bd34975a4d5bab25801eb31898b032dd55addc93e96fcaaa35", + "sha256:fde0b595ca248bb8e2d76f020b465f3b107c9632e6a1d1705f17834c89dcadc0" ], "index": "pypi", - "markers": "python_version >= '3.8'", - "version": "==5.0.0" + "markers": "python_version >= '3.9'", + "version": "==6.0.0" }, "pytest-django": { "hashes": [ @@ -1497,7 +1446,7 @@ "sha256:cbf2f1da5e6083ac2fbfd4da39a25f34312230110440f424a14c7558bb85d82e" ], "index": "pypi", - "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2'", + "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'", "version": "==2.9.0" }, "python-levenshtein": { @@ -1717,106 +1666,6 @@ "markers": "python_version >= '3.9'", "version": "==3.10.1" }, - "regex": { - "hashes": [ - "sha256:02a02d2bb04fec86ad61f3ea7f49c015a0681bf76abb9857f945d26159d2968c", - "sha256:02e28184be537f0e75c1f9b2f8847dc51e08e6e171c6bde130b2687e0c33cf60", - "sha256:040df6fe1a5504eb0f04f048e6d09cd7c7110fef851d7c567a6b6e09942feb7d", - "sha256:068376da5a7e4da51968ce4c122a7cd31afaaec4fccc7856c92f63876e57b51d", - "sha256:06eb1be98df10e81ebaded73fcd51989dcf534e3c753466e4b60c4697a003b67", - "sha256:072623554418a9911446278f16ecb398fb3b540147a7828c06e2011fa531e773", - "sha256:086a27a0b4ca227941700e0b31425e7a28ef1ae8e5e05a33826e17e47fbfdba0", - "sha256:08986dce1339bc932923e7d1232ce9881499a0e02925f7402fb7c982515419ef", - "sha256:0a86e7eeca091c09e021db8eb72d54751e527fa47b8d5787caf96d9831bd02ad", - "sha256:0c32f75920cf99fe6b6c539c399a4a128452eaf1af27f39bce8909c9a3fd8cbe", - "sha256:0d7f453dca13f40a02b79636a339c5b62b670141e63efd511d3f8f73fba162b3", - "sha256:1062b39a0a2b75a9c694f7a08e7183a80c63c0d62b301418ffd9c35f55aaa114", - "sha256:13291b39131e2d002a7940fb176e120bec5145f3aeb7621be6534e46251912c4", - "sha256:149f5008d286636e48cd0b1dd65018548944e495b0265b45e1bffecce1ef7f39", - "sha256:164d8b7b3b4bcb2068b97428060b2a53be050085ef94eca7f240e7947f1b080e", - "sha256:167ed4852351d8a750da48712c3930b031f6efdaa0f22fa1933716bfcd6bf4a3", - "sha256:1c4de13f06a0d54fa0d5ab1b7138bfa0d883220965a29616e3ea61b35d5f5fc7", - "sha256:202eb32e89f60fc147a41e55cb086db2a3f8cb82f9a9a88440dcfc5d37faae8d", - "sha256:220902c3c5cc6af55d4fe19ead504de80eb91f786dc102fbd74894b1551f095e", - "sha256:2b3361af3198667e99927da8b84c1b010752fa4b1115ee30beaa332cabc3ef1a", - "sha256:2c89a8cc122b25ce6945f0423dc1352cb9593c68abd19223eebbd4e56612c5b7", - "sha256:2d548dafee61f06ebdb584080621f3e0c23fff312f0de1afc776e2a2ba99a74f", - "sha256:2e34b51b650b23ed3354b5a07aab37034d9f923db2a40519139af34f485f77d0", - "sha256:32f9a4c643baad4efa81d549c2aadefaeba12249b2adc5af541759237eee1c54", - "sha256:3a51ccc315653ba012774efca4f23d1d2a8a8f278a6072e29c7147eee7da446b", - "sha256:3cde6e9f2580eb1665965ce9bf17ff4952f34f5b126beb509fee8f4e994f143c", - "sha256:40291b1b89ca6ad8d3f2b82782cc33807f1406cf68c8d440861da6304d8ffbbd", - "sha256:41758407fc32d5c3c5de163888068cfee69cb4c2be844e7ac517a52770f9af57", - "sha256:4181b814e56078e9b00427ca358ec44333765f5ca1b45597ec7446d3a1ef6e34", - "sha256:4f51f88c126370dcec4908576c5a627220da6c09d0bff31cfa89f2523843316d", - "sha256:50153825ee016b91549962f970d6a4442fa106832e14c918acd1c8e479916c4f", - "sha256:5056b185ca113c88e18223183aa1a50e66507769c9640a6ff75859619d73957b", - "sha256:5071b2093e793357c9d8b2929dfc13ac5f0a6c650559503bb81189d0a3814519", - "sha256:525eab0b789891ac3be914d36893bdf972d483fe66551f79d3e27146191a37d4", - "sha256:52fb28f528778f184f870b7cf8f225f5eef0a8f6e3778529bdd40c7b3920796a", - "sha256:5478c6962ad548b54a591778e93cd7c456a7a29f8eca9c49e4f9a806dcc5d638", - "sha256:5670bce7b200273eee1840ef307bfa07cda90b38ae56e9a6ebcc9f50da9c469b", - "sha256:5704e174f8ccab2026bd2f1ab6c510345ae8eac818b613d7d73e785f1310f839", - "sha256:59dfe1ed21aea057a65c6b586afd2a945de04fc7db3de0a6e3ed5397ad491b07", - "sha256:5e7e351589da0850c125f1600a4c4ba3c722efefe16b297de54300f08d734fbf", - "sha256:63b13cfd72e9601125027202cad74995ab26921d8cd935c25f09c630436348ff", - "sha256:658f90550f38270639e83ce492f27d2c8d2cd63805c65a13a14d36ca126753f0", - "sha256:684d7a212682996d21ca12ef3c17353c021fe9de6049e19ac8481ec35574a70f", - "sha256:69ab78f848845569401469da20df3e081e6b5a11cb086de3eed1d48f5ed57c95", - "sha256:6f44ec28b1f858c98d3036ad5d7d0bfc568bdd7a74f9c24e25f41ef1ebfd81a4", - "sha256:70b7fa6606c2881c1db9479b0eaa11ed5dfa11c8d60a474ff0e095099f39d98e", - "sha256:764e71f22ab3b305e7f4c21f1a97e1526a25ebdd22513e251cf376760213da13", - "sha256:7ab159b063c52a0333c884e4679f8d7a85112ee3078fe3d9004b2dd875585519", - "sha256:805e6b60c54bf766b251e94526ebad60b7de0c70f70a4e6210ee2891acb70bf2", - "sha256:8447d2d39b5abe381419319f942de20b7ecd60ce86f16a23b0698f22e1b70008", - "sha256:86fddba590aad9208e2fa8b43b4c098bb0ec74f15718bb6a704e3c63e2cef3e9", - "sha256:89d75e7293d2b3e674db7d4d9b1bee7f8f3d1609428e293771d1a962617150cc", - "sha256:93c0b12d3d3bc25af4ebbf38f9ee780a487e8bf6954c115b9f015822d3bb8e48", - "sha256:94d87b689cdd831934fa3ce16cc15cd65748e6d689f5d2b8f4f4df2065c9fa20", - "sha256:9714398225f299aa85267fd222f7142fcb5c769e73d7733344efc46f2ef5cf89", - "sha256:982e6d21414e78e1f51cf595d7f321dcd14de1f2881c5dc6a6e23bbbbd68435e", - "sha256:997d6a487ff00807ba810e0f8332c18b4eb8d29463cfb7c820dc4b6e7562d0cf", - "sha256:a03e02f48cd1abbd9f3b7e3586d97c8f7a9721c436f51a5245b3b9483044480b", - "sha256:a36fdf2af13c2b14738f6e973aba563623cb77d753bbbd8d414d18bfaa3105dd", - "sha256:a6ba92c0bcdf96cbf43a12c717eae4bc98325ca3730f6b130ffa2e3c3c723d84", - "sha256:a7c2155f790e2fb448faed6dd241386719802296ec588a8b9051c1f5c481bc29", - "sha256:a93c194e2df18f7d264092dc8539b8ffb86b45b899ab976aa15d48214138e81b", - "sha256:abfa5080c374a76a251ba60683242bc17eeb2c9818d0d30117b4486be10c59d3", - "sha256:ac10f2c4184420d881a3475fb2c6f4d95d53a8d50209a2500723d831036f7c45", - "sha256:ad182d02e40de7459b73155deb8996bbd8e96852267879396fb274e8700190e3", - "sha256:b2837718570f95dd41675328e111345f9b7095d821bac435aac173ac80b19983", - "sha256:b489578720afb782f6ccf2840920f3a32e31ba28a4b162e13900c3e6bd3f930e", - "sha256:b583904576650166b3d920d2bcce13971f6f9e9a396c673187f49811b2769dc7", - "sha256:b85c2530be953a890eaffde05485238f07029600e8f098cdf1848d414a8b45e4", - "sha256:b97c1e0bd37c5cd7902e65f410779d39eeda155800b65fc4d04cc432efa9bc6e", - "sha256:ba9b72e5643641b7d41fa1f6d5abda2c9a263ae835b917348fc3c928182ad467", - "sha256:bb26437975da7dc36b7efad18aa9dd4ea569d2357ae6b783bf1118dabd9ea577", - "sha256:bb8f74f2f10dbf13a0be8de623ba4f9491faf58c24064f32b65679b021ed0001", - "sha256:bde01f35767c4a7899b7eb6e823b125a64de314a8ee9791367c9a34d56af18d0", - "sha256:bec9931dfb61ddd8ef2ebc05646293812cb6b16b60cf7c9511a832b6f1854b55", - "sha256:c36f9b6f5f8649bb251a5f3f66564438977b7ef8386a52460ae77e6070d309d9", - "sha256:cdf58d0e516ee426a48f7b2c03a332a4114420716d55769ff7108c37a09951bf", - "sha256:d1cee317bfc014c2419a76bcc87f071405e3966da434e03e13beb45f8aced1a6", - "sha256:d22326fcdef5e08c154280b71163ced384b428343ae16a5ab2b3354aed12436e", - "sha256:d3660c82f209655a06b587d55e723f0b813d3a7db2e32e5e7dc64ac2a9e86fde", - "sha256:da8f5fc57d1933de22a9e23eec290a0d8a5927a5370d24bda9a6abe50683fe62", - "sha256:df951c5f4a1b1910f1a99ff42c473ff60f8225baa1cdd3539fe2819d9543e9df", - "sha256:e5364a4502efca094731680e80009632ad6624084aff9a23ce8c8c6820de3e51", - "sha256:ea1bfda2f7162605f6e8178223576856b3d791109f15ea99a9f95c16a7636fb5", - "sha256:f02f93b92358ee3f78660e43b4b0091229260c5d5c408d17d60bf26b6c900e86", - "sha256:f056bf21105c2515c32372bbc057f43eb02aae2fda61052e2f7622c801f0b4e2", - "sha256:f1ac758ef6aebfc8943560194e9fd0fa18bcb34d89fd8bd2af18183afd8da3a2", - "sha256:f2a19f302cd1ce5dd01a9099aaa19cae6173306d1302a43b627f62e21cf18ac0", - "sha256:f654882311409afb1d780b940234208a252322c24a93b442ca714d119e68086c", - "sha256:f65557897fc977a44ab205ea871b690adaef6b9da6afda4790a2484b04293a5f", - "sha256:f9d1e379028e0fc2ae3654bac3cbbef81bf3fd571272a42d56c24007979bafb6", - "sha256:fdabbfc59f2c6edba2a6622c647b716e34e8e3867e0ab975412c5c2f79b82da2", - "sha256:fdd6028445d2460f33136c55eeb1f601ab06d74cb3347132e1c24250187500d9", - "sha256:ff590880083d60acc0433f9c3f713c51f7ac6ebb9adf889c79a261ecf541aa91" - ], - "markers": "python_version >= '3.8'", - "version": "==2024.11.6" - }, "requests": { "hashes": [ "sha256:55365417734eb18255590a9ff9eb97e9e1da868d4ccd6402399eaf68af20a760", @@ -1842,6 +1691,31 @@ "markers": "python_version >= '3.8'", "version": "==0.25.3" }, + "ruff": { + "hashes": [ + "sha256:2029b8c22da147c50ae577e621a5bfbc5d1fed75d86af53643d7a7aee1d23871", + "sha256:2666520828dee7dfc7e47ee4ea0d928f40de72056d929a7c5292d95071d881d1", + "sha256:288326162804f34088ac007139488dcb43de590a5ccfec3166396530b58fb89d", + "sha256:2954cdbe8dfd8ab359d4a30cd971b589d335a44d444b6ca2cb3d1da21b75e4b6", + "sha256:333c57013ef8c97a53892aa56042831c372e0bb1785ab7026187b7abd0135ad5", + "sha256:3583db9a6450364ed5ca3f3b4225958b24f78178908d5c4bc0f46251ccca898f", + "sha256:364e6674450cbac8e998f7b30639040c99d81dfb5bbc6dfad69bc7a8f916b3d1", + "sha256:55873cc1a473e5ac129d15eccb3c008c096b94809d693fc7053f588b67822737", + "sha256:93335cd7c0eaedb44882d75a7acb7df4b77cd7cd0d2255c93b28791716e81790", + "sha256:a885d68342a231b5ba4d30b8c6e1b1ee3a65cf37e3d29b3c74069cdf1ee1e3c9", + "sha256:adf314fc458374c25c5c4a4a9270c3e8a6a807b1bec018cfa2813d6546215540", + "sha256:b12c39b9448632284561cbf4191aa1b005882acbc81900ffa9f9f471c8ff7e26", + "sha256:b22346f845fec132aa39cd29acb94451d030c10874408dbf776af3aaeb53284c", + "sha256:b2f2f7a7e7648a2bfe6ead4e0a16745db956da0e3a231ad443d2a66a105c04fa", + "sha256:b8a4f7385c2285c30f34b200ca5511fcc865f17578383db154e098150ce0a087", + "sha256:cd054486da0c53e41e0086e1730eb77d1f698154f910e0cd9e0d64274979a209", + "sha256:d2c16e3508c8cc73e96aa5127d0df8913d2290098f776416a4b157657bee44c5", + "sha256:fae0805bd514066f20309f6742f6ee7904a773eb9e6c17c45d6b1600ca65c9b5" + ], + "index": "pypi", + "markers": "python_version >= '3.7'", + "version": "==0.8.1" + }, "s3transfer": { "hashes": [ "sha256:244a76a24355363a68164241438de1b72f8781664920260c48465896b712a41e", @@ -1874,7 +1748,7 @@ "sha256:8abb2f1d86890a2dfb989f9a77cfcfd3e47c2a354b01111771326f8aa26e0254" ], "index": "pypi", - "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2'", + "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'", "version": "==1.16.0" }, "sorl-thumbnail": { @@ -1896,10 +1770,10 @@ }, "sure": { "hashes": [ - "sha256:140bc01837255a8c97a05967d868b38af62dcbca94f400ceba604df4f955cf85" + "sha256:c8fc6fabc0e7f6984eeabb942540e45646e5bef0bb99fe59e02da634e4d4b9ca" ], "index": "pypi", - "version": "==3.0a2" + "version": "==2.0.1" }, "testtools": { "hashes": [ @@ -1909,14 +1783,6 @@ "index": "pypi", "version": "==2.3.0" }, - "toml": { - "hashes": [ - "sha256:806143ae5bfb6a3c6e736a764057db0e6a0e05e338b5630894a5f779cabb4f9b", - "sha256:b3bda1d108d5dd99f4a20d24d9c348e91c4db7ab1b749200bded2f839ccbe68f" - ], - "markers": "python_version >= '2.6' and python_version not in '3.0, 3.1, 3.2'", - "version": "==0.10.2" - }, "tqdm": { "hashes": [ "sha256:14baa7a9ea7723d46f60de5f8c6f20e840baa7e3e193bf0d9ec5fe9103a15254", @@ -1933,53 +1799,6 @@ "index": "pypi", "version": "==1.4.0" }, - "typed-ast": { - "hashes": [ - "sha256:042eb665ff6bf020dd2243307d11ed626306b82812aba21836096d229fdc6a10", - "sha256:045f9930a1550d9352464e5149710d56a2aed23a2ffe78946478f7b5416f1ede", - "sha256:0635900d16ae133cab3b26c607586131269f88266954eb04ec31535c9a12ef1e", - "sha256:118c1ce46ce58fda78503eae14b7664163aa735b620b64b5b725453696f2a35c", - "sha256:16f7313e0a08c7de57f2998c85e2a69a642e97cb32f87eb65fbfe88381a5e44d", - "sha256:1efebbbf4604ad1283e963e8915daa240cb4bf5067053cf2f0baadc4d4fb51b8", - "sha256:2188bc33d85951ea4ddad55d2b35598b2709d122c11c75cffd529fbc9965508e", - "sha256:2b946ef8c04f77230489f75b4b5a4a6f24c078be4aed241cfabe9cbf4156e7e5", - "sha256:335f22ccb244da2b5c296e6f96b06ee9bed46526db0de38d2f0e5a6597b81155", - "sha256:381eed9c95484ceef5ced626355fdc0765ab51d8553fec08661dce654a935db4", - "sha256:429ae404f69dc94b9361bb62291885894b7c6fb4640d561179548c849f8492ba", - "sha256:44f214394fc1af23ca6d4e9e744804d890045d1643dd7e8229951e0ef39429b5", - "sha256:48074261a842acf825af1968cd912f6f21357316080ebaca5f19abbb11690c8a", - "sha256:4bc1efe0ce3ffb74784e06460f01a223ac1f6ab31c6bc0376a21184bf5aabe3b", - "sha256:57bfc3cf35a0f2fdf0a88a3044aafaec1d2f24d8ae8cd87c4f58d615fb5b6311", - "sha256:597fc66b4162f959ee6a96b978c0435bd63791e31e4f410622d19f1686d5e769", - "sha256:5f7a8c46a8b333f71abd61d7ab9255440d4a588f34a21f126bbfc95f6049e686", - "sha256:5fe83a9a44c4ce67c796a1b466c270c1272e176603d5e06f6afbc101a572859d", - "sha256:61443214d9b4c660dcf4b5307f15c12cb30bdfe9588ce6158f4a005baeb167b2", - "sha256:622e4a006472b05cf6ef7f9f2636edc51bda670b7bbffa18d26b255269d3d814", - "sha256:6eb936d107e4d474940469e8ec5b380c9b329b5f08b78282d46baeebd3692dc9", - "sha256:7f58fabdde8dcbe764cef5e1a7fcb440f2463c1bbbec1cf2a86ca7bc1f95184b", - "sha256:83509f9324011c9a39faaef0922c6f720f9623afe3fe220b6d0b15638247206b", - "sha256:8c524eb3024edcc04e288db9541fe1f438f82d281e591c548903d5b77ad1ddd4", - "sha256:94282f7a354f36ef5dbce0ef3467ebf6a258e370ab33d5b40c249fa996e590dd", - "sha256:b445c2abfecab89a932b20bd8261488d574591173d07827c1eda32c457358b18", - "sha256:be4919b808efa61101456e87f2d4c75b228f4e52618621c77f1ddcaae15904fa", - "sha256:bfd39a41c0ef6f31684daff53befddae608f9daf6957140228a08e51f312d7e6", - "sha256:c631da9710271cb67b08bd3f3813b7af7f4c69c319b75475436fcab8c3d21bee", - "sha256:cc95ffaaab2be3b25eb938779e43f513e0e538a84dd14a5d844b8f2932593d88", - "sha256:d09d930c2d1d621f717bb217bf1fe2584616febb5138d9b3e8cdd26506c3f6d4", - "sha256:d40c10326893ecab8a80a53039164a224984339b2c32a6baf55ecbd5b1df6431", - "sha256:d41b7a686ce653e06c2609075d397ebd5b969d821b9797d029fccd71fdec8e04", - "sha256:d5c0c112a74c0e5db2c75882a0adf3133adedcdbfd8cf7c9d6ed77365ab90a1d", - "sha256:e1a976ed4cc2d71bb073e1b2a250892a6e968ff02aa14c1f40eba4f365ffec02", - "sha256:e48bf27022897577d8479eaed64701ecaf0467182448bd95759883300ca818c8", - "sha256:ed4a1a42df8a3dfb6b40c3d2de109e935949f2f66b19703eafade03173f8f437", - "sha256:f0aefdd66f1784c58f65b502b6cf8b121544680456d1cebbd300c2c813899274", - "sha256:fc2b8c4e1bc5cd96c1a823a885e6b158f8451cf6f5530e1829390b4d27d0807f", - "sha256:fd946abf3c31fb50eee07451a6aedbfff912fcd13cf357363f5b4e834cc5e71a", - "sha256:fe58ef6a764de7b4b36edfc8592641f56e69b7163bba9f9c8089838ee596bfb2" - ], - "markers": "python_version >= '3.6'", - "version": "==1.5.5" - }, "typing-extensions": { "hashes": [ "sha256:04e5ca0351e0f3f85c6853954072df659d0d13fac324d0072316b67d7794700d", diff --git a/README.md b/README.md index f77eaf6e..1da1b413 100644 --- a/README.md +++ b/README.md @@ -2,7 +2,6 @@ [![Coverage Status](https://coveralls.io/repos/DemocracyClub/electionleaflets/badge.svg?branch=master)](https://coveralls.io/r/DemocracyClub/electionleaflets?branch=django_1_7) [![CI](https://circleci.com/gh/DemocracyClub/electionleaflets.svg?style=shield)](https://app.circleci.com/pipelines/github/DemocracyClub/electionleaflets) -[![Code style: black](https://img.shields.io/badge/code%20style-black-000000.svg)](https://github.com/psf/black) ### Welcome @@ -70,4 +69,4 @@ The app is deployed to AWS using the [Serverless Framework](https://serverless.com/). This manages resources in AWS Lambda and AWS API Gateway. -The app is deployed through CircleCI. The staging deployment is triggered by temporarily adding the current branch to context in the sam-deploy job. The production deployment is triggered by merging to master. \ No newline at end of file +The app is deployed through CircleCI. The staging deployment is triggered by temporarily adding the current branch to context in the sam-deploy job. The production deployment is triggered by merging to master. diff --git a/electionleaflets/apps/analysis/migrations/0001_initial.py b/electionleaflets/apps/analysis/migrations/0001_initial.py index 0535de73..8a3d458b 100644 --- a/electionleaflets/apps/analysis/migrations/0001_initial.py +++ b/electionleaflets/apps/analysis/migrations/0001_initial.py @@ -2,13 +2,12 @@ # Generated by Django 1.9.5 on 2016-04-03 14:56 -from django.db import migrations, models import django.utils.timezone import django_extensions.db.fields +from django.db import migrations, models class Migration(migrations.Migration): - initial = True dependencies = [] diff --git a/electionleaflets/apps/analysis/migrations/0002_auto_20160403_1456.py b/electionleaflets/apps/analysis/migrations/0002_auto_20160403_1456.py index 872673e5..f09431b3 100644 --- a/electionleaflets/apps/analysis/migrations/0002_auto_20160403_1456.py +++ b/electionleaflets/apps/analysis/migrations/0002_auto_20160403_1456.py @@ -2,13 +2,12 @@ # Generated by Django 1.9.5 on 2016-04-03 14:56 +import django.db.models.deletion from django.conf import settings from django.db import migrations, models -import django.db.models.deletion class Migration(migrations.Migration): - initial = True dependencies = [ diff --git a/electionleaflets/apps/analysis/migrations/0003_add_missing_people.py b/electionleaflets/apps/analysis/migrations/0003_add_missing_people.py index 5b97b1f5..7b5ff4a6 100644 --- a/electionleaflets/apps/analysis/migrations/0003_add_missing_people.py +++ b/electionleaflets/apps/analysis/migrations/0003_add_missing_people.py @@ -2,7 +2,6 @@ # Generated by Django 1.11.23 on 2019-12-02 23:06 import requests - from django.db import migrations @@ -18,7 +17,6 @@ def add_missing_people(apps, schema_editor): remote_id=leaflet.publisher_person_id ) except Person.DoesNotExist: - url = "https://candidates.democracyclub.org.uk/api/next/people/{}/".format( leaflet.publisher_person_id ) @@ -32,7 +30,6 @@ def add_missing_people(apps, schema_editor): class Migration(migrations.Migration): - dependencies = [ ("analysis", "0002_auto_20160403_1456"), ("people", "0001_initial"), diff --git a/electionleaflets/apps/analysis/migrations/0004_auto_20210424_1221.py b/electionleaflets/apps/analysis/migrations/0004_auto_20210424_1221.py index a4fad777..f5f06792 100644 --- a/electionleaflets/apps/analysis/migrations/0004_auto_20210424_1221.py +++ b/electionleaflets/apps/analysis/migrations/0004_auto_20210424_1221.py @@ -1,11 +1,10 @@ # Generated by Django 2.2.20 on 2021-04-24 12:21 -from django.db import migrations import django_extensions.db.fields +from django.db import migrations class Migration(migrations.Migration): - dependencies = [ ("analysis", "0003_add_missing_people"), ] diff --git a/electionleaflets/apps/analysis/migrations/0005_alter_leafletproperties_options.py b/electionleaflets/apps/analysis/migrations/0005_alter_leafletproperties_options.py index fe337e96..def349c9 100644 --- a/electionleaflets/apps/analysis/migrations/0005_alter_leafletproperties_options.py +++ b/electionleaflets/apps/analysis/migrations/0005_alter_leafletproperties_options.py @@ -4,14 +4,13 @@ class Migration(migrations.Migration): - dependencies = [ - ('analysis', '0004_auto_20210424_1221'), + ("analysis", "0004_auto_20210424_1221"), ] operations = [ migrations.AlterModelOptions( - name='leafletproperties', - options={'get_latest_by': 'modified'}, + name="leafletproperties", + options={"get_latest_by": "modified"}, ), ] diff --git a/electionleaflets/apps/analysis/models.py b/electionleaflets/apps/analysis/models.py index 86ba3e18..0dde6a43 100644 --- a/electionleaflets/apps/analysis/models.py +++ b/electionleaflets/apps/analysis/models.py @@ -1,8 +1,6 @@ +from django.contrib.auth.models import User from django.db import models from django_extensions.db.models import TimeStampedModel - -from django.contrib.auth.models import User - from leaflets.models import Leaflet diff --git a/electionleaflets/apps/api/feeds.py b/electionleaflets/apps/api/feeds.py index 1ff5b49d..f56afec9 100644 --- a/electionleaflets/apps/api/feeds.py +++ b/electionleaflets/apps/api/feeds.py @@ -1,13 +1,11 @@ # -*- coding: utf-8 -*- -import mimetypes +# from parties.models import Party +from constituencies.models import Constituency from django.contrib.syndication.views import Feed from django.shortcuts import get_object_or_404 from leaflets.models import Leaflet -# from parties.models import Party -from constituencies.models import Constituency - class LatestLeafletsFeed(Feed): title = "electionleaflets.org latest items" @@ -28,6 +26,7 @@ def item_description(self, item): d = "{0} – {1}".format(d, item.images.all()[0].image.url) return d + class ConstituencyFeed(Feed): def get_object(self, request, cons_slug): obj = get_object_or_404(Constituency, slug=cons_slug) diff --git a/electionleaflets/apps/api/serializers.py b/electionleaflets/apps/api/serializers.py index f2907d88..83517a0e 100644 --- a/electionleaflets/apps/api/serializers.py +++ b/electionleaflets/apps/api/serializers.py @@ -1,10 +1,8 @@ +from leaflets.models import Leaflet, LeafletImage +from people.models import Person from rest_framework import serializers from sorl.thumbnail import get_thumbnail - -from leaflets.models import Leaflet, LeafletImage -from constituencies.models import Constituency from uk_political_parties.models import Party -from people.models import Person class PartySerializer(serializers.HyperlinkedModelSerializer): @@ -58,8 +56,9 @@ def get_people(self, obj): def get_first_page_thumb(self, obj): image = obj.get_first_image() - if image: - return get_thumbnail(obj.get_first_image().image, "350").url + if not image: + return None + return get_thumbnail(obj.get_first_image().image, "350").url def validate(self, data): if not data.get("status") or not data.get("images"): @@ -89,8 +88,9 @@ class LeafletMinSerializer(serializers.ModelSerializer): def get_first_page_thumb(self, obj): image = obj.get_first_image() - if image: - return get_thumbnail(obj.get_first_image().image, "350").url + if not image: + return None + return get_thumbnail(obj.get_first_image().image, "350").url class Meta: model = Leaflet diff --git a/electionleaflets/apps/api/tests/test_feeds.py b/electionleaflets/apps/api/tests/test_feeds.py index 2a192ae9..d11837b2 100644 --- a/electionleaflets/apps/api/tests/test_feeds.py +++ b/electionleaflets/apps/api/tests/test_feeds.py @@ -1,19 +1,20 @@ import pytest -from django.contrib.auth.models import User - from api.feeds import ConstituencyFeed, LatestLeafletsFeed from constituencies.models import Constituency from leaflets.tests.model_factory import LeafletFactory + @pytest.fixture(autouse=True) def create_leaflets(): for _ in range(10): LeafletFactory() + @pytest.fixture def create_constituency(): return Constituency.objects.create(name="test_name", slug="test_slug") + @pytest.mark.django_db class TestLatestLeafletsFeed: def test_items_order(self, create_leaflets): @@ -33,8 +34,13 @@ def test_item_description(self, create_leaflets): if item.description: description = item.description if item.images.all(): - description = "{0} – {1}".format(description, item.images.all()[0].image.url) - assert LatestLeafletsFeed.item_description(self, item) == description + description = "{0} – {1}".format( + description, item.images.all()[0].image.url + ) + assert ( + LatestLeafletsFeed.item_description(self, item) == description + ) + @pytest.mark.django_db class TestConstituencyFeed: @@ -43,7 +49,10 @@ def test_get_object(self, create_constituency): obj = feed.get_object(None, create_constituency.slug) assert obj.slug == create_constituency.slug assert feed.link == "/constituencies/%s/" % obj.slug - assert feed.description == "The most recently uploaded leaflets for %s" % obj.name + assert ( + feed.description + == "The most recently uploaded leaflets for %s" % obj.name + ) def test_items(self, create_leaflets, create_constituency): LeafletFactory(constituency=create_constituency) diff --git a/electionleaflets/apps/api/tests/test_views.py b/electionleaflets/apps/api/tests/test_views.py index 24510bd0..a5c1fd46 100644 --- a/electionleaflets/apps/api/tests/test_views.py +++ b/electionleaflets/apps/api/tests/test_views.py @@ -1,28 +1,29 @@ import pytest -from electionleaflets.apps.api.views import LeafletFilter -from leaflets.models import Leaflet from django.utils import timezone +from leaflets.models import Leaflet from uk_political_parties.models import Party +from electionleaflets.apps.api.views import LeafletFilter + + @pytest.fixture def all_leaflets(): - return [ - Leaflet.objects.create( - title=f"Test Leaflet {i}", - status="live", - date_uploaded=timezone.now(), - modified=timezone.now() - ) - for i in range(10) - ] - + return [ + Leaflet.objects.create( + title=f"Test Leaflet {i}", + status="live", + date_uploaded=timezone.now(), + modified=timezone.now(), + ) + for i in range(10) + ] + + @pytest.fixture def party(): - return Party.objects.create( - party_name="Test Party", - party_id="party:1" - ) - + return Party.objects.create(party_name="Test Party", party_id="party:1") + + @pytest.fixture def leaflet_with_party(all_leaflets, party): leaflet = all_leaflets[0] @@ -30,17 +31,31 @@ def leaflet_with_party(all_leaflets, party): leaflet.save() return leaflet + @pytest.mark.django_db class TestLeafletFilter: def test_ballot_filter(self, all_leaflets): leaflet = all_leaflets[0] leaflet.ballots = [{"ballot_paper_id": "test"}] leaflet.save() - filter = LeafletFilter(data={"ballot": "test"}, queryset=Leaflet.objects.all()) - assert filter.ballot_filter(Leaflet.objects.all(), "ballot", "test").count() == 1 - + filter = LeafletFilter( + data={"ballot": "test"}, queryset=Leaflet.objects.all() + ) + assert ( + filter.ballot_filter( + Leaflet.objects.all(), "ballot", "test" + ).count() + == 1 + ) + def test_party_filter(self, leaflet_with_party): party = Party.objects.get(party_id="party:1") - filter = LeafletFilter(data={"party": party}, queryset=Leaflet.objects.all()) - assert filter.party_filter(Leaflet.objects.all(), party, party.party_id).count() == 1 - \ No newline at end of file + filter = LeafletFilter( + data={"party": party}, queryset=Leaflet.objects.all() + ) + assert ( + filter.party_filter( + Leaflet.objects.all(), party, party.party_id + ).count() + == 1 + ) diff --git a/electionleaflets/apps/api/views.py b/electionleaflets/apps/api/views.py index 9bce524d..7bf52085 100644 --- a/electionleaflets/apps/api/views.py +++ b/electionleaflets/apps/api/views.py @@ -2,10 +2,10 @@ from django.db.models import Q from django_filters import rest_framework as filters +from leaflets.models import Leaflet from rest_framework import viewsets from rest_framework.pagination import LimitOffsetPagination -from leaflets.models import Leaflet from .serializers import LeafletSerializer @@ -17,10 +17,7 @@ class StandardResultsSetPagination(LimitOffsetPagination): class LeafletFilter(filters.FilterSet): class Meta: model = Leaflet - fields = { - "date_uploaded": ["gt", "exact"], - "modified": ["gt", "exact"] - } + fields = {"date_uploaded": ["gt", "exact"], "modified": ["gt", "exact"]} def ballot_filter(self, queryset, name, value): return queryset.filter(ballots__contains=[{"ballot_paper_id": value}]) @@ -43,7 +40,9 @@ def party_filter(self, queryset, name, value): class LeafletViewSet(viewsets.ReadOnlyModelViewSet): - queryset = Leaflet.objects.all().filter(status="live").prefetch_related("images") + queryset = ( + Leaflet.objects.all().filter(status="live").prefetch_related("images") + ) serializer_class = LeafletSerializer pagination_class = StandardResultsSetPagination filter_backends = (filters.DjangoFilterBackend,) diff --git a/electionleaflets/apps/constituencies/migrations/0001_initial.py b/electionleaflets/apps/constituencies/migrations/0001_initial.py index 2d305a05..f0bac786 100644 --- a/electionleaflets/apps/constituencies/migrations/0001_initial.py +++ b/electionleaflets/apps/constituencies/migrations/0001_initial.py @@ -2,12 +2,11 @@ # Generated by Django 1.9.5 on 2016-04-03 14:56 -from django.db import migrations, models import django_extensions.db.fields +from django.db import migrations, models class Migration(migrations.Migration): - initial = True dependencies = [] @@ -51,6 +50,8 @@ class Migration(migrations.Migration): models.IntegerField(blank=True, null=True), ), ], - options={"verbose_name_plural": "Constituencies",}, + options={ + "verbose_name_plural": "Constituencies", + }, ), ] diff --git a/electionleaflets/apps/constituencies/models.py b/electionleaflets/apps/constituencies/models.py index 539740a0..e6d124c9 100644 --- a/electionleaflets/apps/constituencies/models.py +++ b/electionleaflets/apps/constituencies/models.py @@ -27,5 +27,10 @@ def __unicode__(self): def get_absolute_url(self): return reverse( - "constituency-view", (), {"pk": self.pk, "ignored_slug": self.slug,} + "constituency-view", + (), + { + "pk": self.pk, + "ignored_slug": self.slug, + }, ) diff --git a/electionleaflets/apps/core/admin.py b/electionleaflets/apps/core/admin.py index 3a8c4a09..c46ce044 100644 --- a/electionleaflets/apps/core/admin.py +++ b/electionleaflets/apps/core/admin.py @@ -1,5 +1,5 @@ -from django.contrib import admin from core.models import Country, EmailAlert +from django.contrib import admin class CountryOptions(admin.ModelAdmin): diff --git a/electionleaflets/apps/core/helpers.py b/electionleaflets/apps/core/helpers.py index 2bdacaf6..0da2dc38 100644 --- a/electionleaflets/apps/core/helpers.py +++ b/electionleaflets/apps/core/helpers.py @@ -1,11 +1,9 @@ import requests - +from constituencies.models import Constituency from django.conf import settings from django.core.cache import cache from django.utils.cache import patch_response_headers -from constituencies.models import Constituency - def geocode(postcode): """ @@ -24,12 +22,11 @@ def geocode(postcode): res_json = res.json() if "code" in res_json and res_json["code"] == 404: return None - else: - constituency = Constituency.objects.get( - constituency_id=str(res_json["shortcuts"]["WMC"]) - ) - lat = res_json["wgs84_lat"] - lon = res_json["wgs84_lon"] + constituency = Constituency.objects.get( + constituency_id=str(res_json["shortcuts"]["WMC"]) + ) + lat = res_json["wgs84_lat"] + lon = res_json["wgs84_lon"] result = { "wgs84_lon": lon, diff --git a/electionleaflets/apps/core/migrations/0001_initial.py b/electionleaflets/apps/core/migrations/0001_initial.py index 56c1cbb2..4192c946 100644 --- a/electionleaflets/apps/core/migrations/0001_initial.py +++ b/electionleaflets/apps/core/migrations/0001_initial.py @@ -6,7 +6,6 @@ class Migration(migrations.Migration): - initial = True dependencies = [] @@ -61,7 +60,9 @@ class Migration(migrations.Migration): ("postcode", models.CharField(max_length=60)), ("delivery_date", models.DateTimeField()), ], - options={"db_table": "email_que",}, + options={ + "db_table": "email_que", + }, ), migrations.CreateModel( name="ImageQue", @@ -76,7 +77,9 @@ class Migration(migrations.Migration): ("image_key", models.CharField(blank=True, max_length=765)), ("uploaded_date", models.DateTimeField()), ], - options={"db_table": "image_que",}, + options={ + "db_table": "image_que", + }, ), migrations.CreateModel( name="ImageQueSeq", @@ -86,6 +89,8 @@ class Migration(migrations.Migration): models.IntegerField(primary_key=True, serialize=False), ), ], - options={"db_table": "image_que_seq",}, + options={ + "db_table": "image_que_seq", + }, ), ] diff --git a/electionleaflets/apps/core/s3_thumbnail_store.py b/electionleaflets/apps/core/s3_thumbnail_store.py index efee6530..d9da8294 100644 --- a/electionleaflets/apps/core/s3_thumbnail_store.py +++ b/electionleaflets/apps/core/s3_thumbnail_store.py @@ -1,6 +1,6 @@ +from sorl.thumbnail import default from sorl.thumbnail.base import ThumbnailBackend from sorl.thumbnail.images import ImageFile -from sorl.thumbnail import default class S3Backend(ThumbnailBackend): @@ -20,8 +20,7 @@ def get_thumbnail(self, file_, geometry_string, **options): options.setdefault(key, value) name = self._get_thumbnail_filename(source, geometry_string, options) - thumbnail = ImageFile(name, default.storage) - return thumbnail + return ImageFile(name, default.storage) def _get_thumbnail_filename(self, source, geometry_string, options): """ @@ -42,10 +41,11 @@ def _get_thumbnail_filename(self, source, geometry_string, options): ["{}={}".format(k, v) for k, v in list(opts.items())] ) - thumb_url = "{base_url}/{geometry_string}/{url_kwargs}/{original_path}".format( - base_url=base_url, - geometry_string=geometry_string, - url_kwargs=url_kwargs, - original_path=source.name, + return ( + "{base_url}/{geometry_string}/{url_kwargs}/{original_path}".format( + base_url=base_url, + geometry_string=geometry_string, + url_kwargs=url_kwargs, + original_path=source.name, + ) ) - return thumb_url diff --git a/electionleaflets/apps/core/storage_helpers.py b/electionleaflets/apps/core/storage_helpers.py index 367ac625..50680ec5 100644 --- a/electionleaflets/apps/core/storage_helpers.py +++ b/electionleaflets/apps/core/storage_helpers.py @@ -1,9 +1,4 @@ -from django.core.files.base import ContentFile -from django.core.files.uploadedfile import UploadedFile -from django.utils.datastructures import MultiValueDict -from formtools.wizard.storage import NoFileStorageConfigured from formtools.wizard.storage.session import SessionStorage -from storages.backends.s3boto3 import S3Boto3StorageFile class PreUploadedSessionStorage(SessionStorage): diff --git a/electionleaflets/apps/core/templatetags/markdown.py b/electionleaflets/apps/core/templatetags/markdown.py index 332d9fdd..a41e5a69 100644 --- a/electionleaflets/apps/core/templatetags/markdown.py +++ b/electionleaflets/apps/core/templatetags/markdown.py @@ -10,4 +10,4 @@ def markdown_filter(text): return mark_safe(markdown.markdown(text)) -markdown.is_safe = True \ No newline at end of file +markdown.is_safe = True diff --git a/electionleaflets/apps/core/test_forms.py b/electionleaflets/apps/core/test_forms.py index 7f964aee..72e26cb0 100644 --- a/electionleaflets/apps/core/test_forms.py +++ b/electionleaflets/apps/core/test_forms.py @@ -5,27 +5,34 @@ @pytest.mark.django_db def test_form_valid_data(): - form = ReportAbuseForm(data={ - 'name': 'Sam', - 'details': 'This is not a spam leaflet.', - 'email': 'user@example.com', - }) + form = ReportAbuseForm( + data={ + "name": "Sam", + "details": "This is not a spam leaflet.", + "email": "user@example.com", + } + ) assert form.is_valid() + @pytest.mark.django_db def test_form_invalid_data(): - form = ReportAbuseForm(data={ - 'name': '', - 'details': 'This is a spam leaflet.', - 'email': 'user@example.com', - }) + form = ReportAbuseForm( + data={ + "name": "", + "details": "This is a spam leaflet.", + "email": "user@example.com", + } + ) assert not form.is_valid() + @pytest.mark.django_db def test_form_missing_data(): - form = ReportAbuseForm(data={ - 'name': 'Sam', - 'details': 'This is a spam leaflet.', - }) + form = ReportAbuseForm( + data={ + "name": "Sam", + "details": "This is a spam leaflet.", + } + ) assert not form.is_valid() - diff --git a/electionleaflets/apps/core/test_models.py b/electionleaflets/apps/core/test_models.py index 8e9a6635..3e4ad07d 100644 --- a/electionleaflets/apps/core/test_models.py +++ b/electionleaflets/apps/core/test_models.py @@ -37,7 +37,7 @@ class Meta: name = factory.Sequence(lambda n: "Name %d" % n) email = factory.Sequence(lambda n: "user%d@example.com" % n) postcode = factory.Sequence(lambda n: "Postcode %d" % n) - delivery_date = factory.Faker('date_time') + delivery_date = factory.Faker("date_time") class ImageQueFactory(DjangoModelFactory): @@ -49,7 +49,7 @@ class Meta: name = factory.Sequence(lambda n: "Name %d" % n) email = factory.Sequence(lambda n: "user%d@example.com" % n) image_key = factory.Sequence(lambda n: "ImageKey%d" % n) - uploaded_date = factory.Faker('date_time') + uploaded_date = factory.Faker("date_time") class ImageQueSeqFactory(DjangoModelFactory): @@ -58,6 +58,7 @@ class Meta: sequence = factory.Sequence(lambda n: n) + @pytest.mark.django_db def test_country_creation(): country = CountryFactory() diff --git a/electionleaflets/apps/core/views.py b/electionleaflets/apps/core/views.py index c4760e0a..8744c8a4 100644 --- a/electionleaflets/apps/core/views.py +++ b/electionleaflets/apps/core/views.py @@ -1,16 +1,13 @@ # -*- coding: utf-8 -*- -import datetime -from django.http import HttpResponseRedirect -from django.urls import reverse -from django.views.decorators.cache import cache_control -from django.views.generic import FormView, TemplateView, DetailView +from constituencies.models import Constituency from django.conf import settings +from django.contrib.sites.models import Site from django.core.mail import EmailMultiAlternatives +from django.http import HttpResponseRedirect from django.template.loader import render_to_string -from django.contrib.sites.models import Site - -from constituencies.models import Constituency +from django.urls import reverse +from django.views.generic import DetailView, FormView, TemplateView from leaflets.models import Leaflet, LeafletImage from people.models import Person from uk_political_parties.models import Party @@ -24,7 +21,6 @@ class HomeView(CacheControlMixin, TemplateView): cache_timeout = 60 * 5 def get_context_data(self, **kwargs): - leaflet_count = Leaflet.objects.all().count() context = super(HomeView, self).get_context_data(**kwargs) @@ -55,7 +51,10 @@ def get(self, request, *args, **kwargs): form_class = self.get_form_class() form = self.get_form(form_class) self.object = self.get_object() - context = self.get_context_data(form=form, object=self.object,) + context = self.get_context_data( + form=form, + object=self.object, + ) return self.render_to_response(context) def post(self, request, *args, **kwargs): @@ -66,7 +65,10 @@ def form_valid(self, form): domain = Site.objects.get_current().domain ctx = { "link": "http://%s%s" - % (domain, reverse("leaflet", kwargs={"pk": self.object.id}),), + % ( + domain, + reverse("leaflet", kwargs={"pk": self.object.id}), + ), "name": form.cleaned_data["name"], "email": form.cleaned_data["email"], "details": form.cleaned_data["details"], diff --git a/electionleaflets/apps/elections/migrations/0001_initial.py b/electionleaflets/apps/elections/migrations/0001_initial.py index d1a518c4..d903c43d 100644 --- a/electionleaflets/apps/elections/migrations/0001_initial.py +++ b/electionleaflets/apps/elections/migrations/0001_initial.py @@ -2,12 +2,11 @@ # Generated by Django 1.9.5 on 2016-04-03 14:56 -from django.db import migrations, models import django.db.models.deletion +from django.db import migrations, models class Migration(migrations.Migration): - initial = True dependencies = [ diff --git a/electionleaflets/apps/leaflets/admin.py b/electionleaflets/apps/leaflets/admin.py index 96c14092..1a8d29d4 100644 --- a/electionleaflets/apps/leaflets/admin.py +++ b/electionleaflets/apps/leaflets/admin.py @@ -1,8 +1,8 @@ from django.contrib import admin from leaflets.models import Leaflet, LeafletImage +from sorl.thumbnail import get_thumbnail from .admin_widgets import AdminImageMixin -from sorl.thumbnail import get_thumbnail class LeafletImageInline(AdminImageMixin, admin.TabularInline): @@ -59,9 +59,10 @@ def get_leaflet_title(self, obj): get_leaflet_title.short_description = "Leaflet title" def thumbnail(self, obj): - if obj.image: - thumb = get_thumbnail(obj.image, "100x100", crop="center") - return "" % thumb.url + if not obj.image: + return None + thumb = get_thumbnail(obj.image, "100x100", crop="center") + return "" % thumb.url thumbnail.allow_tags = True diff --git a/electionleaflets/apps/leaflets/admin_widgets/__init__.py b/electionleaflets/apps/leaflets/admin_widgets/__init__.py index de42000b..08f23ab6 100644 --- a/electionleaflets/apps/leaflets/admin_widgets/__init__.py +++ b/electionleaflets/apps/leaflets/admin_widgets/__init__.py @@ -1,3 +1,4 @@ +import contextlib import logging from django import forms @@ -5,7 +6,6 @@ from sorl.thumbnail.fields import ImageField from sorl.thumbnail.shortcuts import get_thumbnail - logger = logging.getLogger(__name__) @@ -39,15 +39,13 @@ def render(self, name, value, attrs=None, **kwargs): except Exception as e: logger.warning("Unable to get the thumbnail", exc_info=e) else: - try: + with contextlib.suppress(AttributeError, TypeError): output = ( '
' '' '%s
' ) % (value.url, mini.url, output) - except (AttributeError, TypeError): - pass return mark_safe(output) diff --git a/electionleaflets/apps/leaflets/features/steps.py b/electionleaflets/apps/leaflets/features/steps.py index 8fef0c15..6e96863b 100644 --- a/electionleaflets/apps/leaflets/features/steps.py +++ b/electionleaflets/apps/leaflets/features/steps.py @@ -2,14 +2,12 @@ import os import re -from django.urls import reverse -from lxml import html - -from django.test.client import Client from aloe import before, step, world from aloe_django import django_url - from constituencies.models import Constituency +from django.test.client import Client +from django.urls import reverse +from lxml import html @before.all @@ -22,7 +20,10 @@ def add_constituency(step, name): Constituency.objects.get_or_create( slug="camberwell_and_peckham", pk="65913", - defaults={"name": "Camberwell and Peckham", "country_name": "England",}, + defaults={ + "name": "Camberwell and Peckham", + "country_name": "England", + }, ) @@ -48,7 +49,8 @@ def set_file(step, name): os.path.join(os.path.dirname(__file__), "../tests/test_images/", name) ) if os.path.exists(file_path): - world.data = {"image": open(file_path, "rb")} + with open(file_path, "rb") as f: + world.data = {"image": f} assert True return assert False @@ -89,9 +91,11 @@ def fill_journey(step): form["leaflet_upload_wizzard-current_step"] = form_name if "action" in list(form.keys()) and form["action"]: form[form["action"]] = True - if "people-people" in list(form.keys()): - if form["people-people"] == "None": - form["people-people"] = "" + if ( + "people-people" in list(form.keys()) + and form["people-people"] == "None" + ): + form["people-people"] = "" world.response = world.browser.post( reverse("upload_step", kwargs={"step": form_name}), diff --git a/electionleaflets/apps/leaflets/fields.py b/electionleaflets/apps/leaflets/fields.py index a60c707a..37d9c5e0 100644 --- a/electionleaflets/apps/leaflets/fields.py +++ b/electionleaflets/apps/leaflets/fields.py @@ -3,7 +3,7 @@ from django import forms from django.core.exceptions import ValidationError -from django.forms.widgets import MultiWidget, NumberInput +from django.forms.widgets import MultiWidget __all__ = ("DayMonthYearWidget",) @@ -29,7 +29,6 @@ def decompress(self, value): class DCDateField(forms.MultiValueField): - widget = DayMonthYearWidget def __init__(self, *args, **kwargs): @@ -45,13 +44,13 @@ def __init__(self, *args, **kwargs): error_messages=error_messages, fields=fields, require_all_fields=True, - **kwargs + **kwargs, ) self.field_class = "form-date" def compress(self, data_list): if not data_list: - return + return None data_list = list(data_list) data_list.reverse() return datetime.datetime(*map(int, data_list)) diff --git a/electionleaflets/apps/leaflets/forms.py b/electionleaflets/apps/leaflets/forms.py index 2594b975..d8ba1434 100644 --- a/electionleaflets/apps/leaflets/forms.py +++ b/electionleaflets/apps/leaflets/forms.py @@ -1,20 +1,16 @@ # coding=utf-8 -import datetime import json from datetime import timedelta from urllib.parse import urljoin import requests from django import forms -from django.core.exceptions import ValidationError -from django.core.signing import Signer from django.conf import settings +from django.core.signing import Signer from django.utils import timezone - -from localflavor.gb.forms import GBPostcodeField - from leaflets.fields import DCDateField from leaflets.models import Leaflet, LeafletImage +from localflavor.gb.forms import GBPostcodeField class S3UploadedImageField(forms.ImageField): @@ -25,6 +21,7 @@ def to_python(self, data): data.name = content if content.startswith("tmp/s3file"): return data + return None class ImagesForm(forms.Form): @@ -35,7 +32,9 @@ def __init__(self, **kwargs): else: self.fields["image"] = S3UploadedImageField( widget=forms.ClearableFileInput( - attrs={"accept": "image/*",} + attrs={ + "accept": "image/*", + } ), error_messages={ "required": "Please add a photo or skip this step" @@ -109,7 +108,8 @@ def create_option( label = "Not Listed" else: label = "{0} ({1})".format( - label["person"]["name"], label["party"]["party_name"], + label["person"]["name"], + label["party"]["party_name"], ) return super(PeopleRadioWidget, self).create_option( name, value, label, selected, index, subindex, attrs @@ -134,7 +134,7 @@ def get_ballot_data_from_ynr(self, postcode): :type instance: Leaflet """ url = urljoin(settings.YNR_BASE_URL, "/api/next/ballots/") - auth_token = getattr(settings, 'YNR_API_KEY') + auth_token = getattr(settings, "YNR_API_KEY") params = {"for_postcode": postcode, "auth_token": auth_token} start, end = self.get_date_range() params["election_date_range_after"] = start @@ -188,7 +188,7 @@ def get_people_from_ballot_data(self, ballot_data, party=None): for ballot in ballot_data: for candidacy in ballot["candidacies"]: - if party and not candidacy["party"]["legacy_slug"] in party: + if party and candidacy["party"]["legacy_slug"] not in party: continue candidacy["ballot"] = { "ballot_paper_id": ballot["ballot_paper_id"], @@ -203,14 +203,15 @@ def get_people_from_ballot_data(self, ballot_data, party=None): class PartyForm(YNRBallotDataMixin, forms.Form): - party = forms.ChoiceField( - choices=[], widget=forms.RadioSelect, required=False, + choices=[], + widget=forms.RadioSelect, + required=False, ) def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) - if not "postcode" in self.initial: + if "postcode" not in self.initial: return self.FOR_DATE = kwargs.get("initial", {}).pop("for_date", None) diff --git a/electionleaflets/apps/leaflets/management/commands/cache_images.py b/electionleaflets/apps/leaflets/management/commands/cache_images.py index bfe721d5..6ca7d716 100644 --- a/electionleaflets/apps/leaflets/management/commands/cache_images.py +++ b/electionleaflets/apps/leaflets/management/commands/cache_images.py @@ -1,9 +1,8 @@ from django.core.management.base import BaseCommand +from leaflets.models import LeafletImage from sorl.thumbnail import get_thumbnail from tqdm import tqdm -from leaflets.models import LeafletImage - class Command(BaseCommand): def handle(self, **options): @@ -13,5 +12,5 @@ def handle(self, **options): try: get_thumbnail(leaflet_image.image, "350").url get_thumbnail(leaflet_image.image, "100x100", crop="center").url - except: + except Exception: pass diff --git a/electionleaflets/apps/leaflets/management/commands/export_meata_data.py b/electionleaflets/apps/leaflets/management/commands/export_meata_data.py index d6ad57bc..3b4a8ecd 100644 --- a/electionleaflets/apps/leaflets/management/commands/export_meata_data.py +++ b/electionleaflets/apps/leaflets/management/commands/export_meata_data.py @@ -1,10 +1,8 @@ # -*- coding: utf-8 -*- import csv - from django.core.management.base import BaseCommand from django.utils.encoding import force_text - from leaflets.models import LeafletImage @@ -34,10 +32,8 @@ def handle(self, **options): "status", "reviewed", ] - - out = csv.DictWriter( - open("/tmp/meta_data.csv", "wb"), fieldnames=fieldnames - ) + with open("/tmp/meta_data.csv", "wb") as f: + out = csv.DictWriter(f, fieldnames=fieldnames) out.writeheader() for image in LeafletImage.objects.all(): data = { @@ -77,7 +73,9 @@ def handle(self, **options): ) if image.leaflet.election: data.update( - {"election": image.leaflet.election,} + { + "election": image.leaflet.election, + } ) for k, v in list(data.items()): diff --git a/electionleaflets/apps/leaflets/management/commands/leaflets_import_legacy.py b/electionleaflets/apps/leaflets/management/commands/leaflets_import_legacy.py index 06cb4af9..8dd46de9 100644 --- a/electionleaflets/apps/leaflets/management/commands/leaflets_import_legacy.py +++ b/electionleaflets/apps/leaflets/management/commands/leaflets_import_legacy.py @@ -2,14 +2,12 @@ import os import re -from django.core.management.base import BaseCommand +from constituencies.models import Constituency from django.conf import settings from django.core.files import File - -from legacy.models import legacyLeaflet - +from django.core.management.base import BaseCommand from leaflets.models import Leaflet, LeafletImage -from constituencies.models import Constituency +from legacy.models import legacyLeaflet from uk_political_parties.models import Party @@ -40,19 +38,20 @@ def clean_legacy_leaflet_image(self, legacy_image): if os.path.exists(image_path): print("Exists") - f = open(image_path, "r") - data["image"] = File(f) + with open(image_path, "r") as f: + data["image"] = File(f) else: image_path = os.path.join( settings.IMAGE_LOCAL_CACHE, "large", key ) if os.path.exists(image_path): print("Exists") - f = open(image_path, "r") - data["image"] = File(f) + with open(image_path, "r") as f: + data["image"] = File(f) else: print("Doesn't exist") return data + return None def clean_constituency(self, con): con_name = con.constituency.name @@ -70,34 +69,30 @@ def clean_constituency(self, con): return con def clean_postcode(self, postcode): - try: - postcode = postcode.encode("utf-8") - postcode = postcode.upper() - postcode = postcode.replace("!", "1") - postcode = postcode.replace("@", "2") - postcode = postcode.replace('"', "2") - postcode = postcode.replace("£", "3") - postcode = postcode.replace("$", "4") - postcode = postcode.replace("%", "5") - postcode = postcode.replace("^", "6") - postcode = postcode.replace("&", "7") - postcode = postcode.replace("*", "8") - postcode = postcode.replace("(", "9") - postcode = postcode.replace(")", "0") - postcode = re.sub(r"[^\x00-\x7F]+", " ", postcode) - postcode = re.sub("^[A-Z0-9\s]", "", postcode) - postcode = postcode.strip() - except: - import ipdb - - ipdb.set_trace() - return postcode + postcode = postcode.encode("utf-8") + postcode = postcode.upper() + postcode = postcode.replace("!", "1") + postcode = postcode.replace("@", "2") + postcode = postcode.replace('"', "2") + postcode = postcode.replace("£", "3") + postcode = postcode.replace("$", "4") + postcode = postcode.replace("%", "5") + postcode = postcode.replace("^", "6") + postcode = postcode.replace("&", "7") + postcode = postcode.replace("*", "8") + postcode = postcode.replace("(", "9") + postcode = postcode.replace(")", "0") + postcode = re.sub(r"[^\x00-\x7F]+", " ", postcode) + postcode = re.sub("^[A-Z0-9\s]", "", postcode) + return postcode.strip() def handle(self, **options): for legacy_leaflet in legacyLeaflet.objects.all(): - if not legacy_leaflet.date_uploaded: - if legacy_leaflet.date_delivered: - legacy_leaflet.date_uploaded = legacy_leaflet.date_delivered + if ( + not legacy_leaflet.date_uploaded + and legacy_leaflet.date_delivered + ): + legacy_leaflet.date_uploaded = legacy_leaflet.date_delivered if legacy_leaflet.date_uploaded: if not bool(legacy_leaflet.publisher_party_id): diff --git a/electionleaflets/apps/leaflets/middleware.py b/electionleaflets/apps/leaflets/middleware.py index 7aa0966e..2b369cb9 100644 --- a/electionleaflets/apps/leaflets/middleware.py +++ b/electionleaflets/apps/leaflets/middleware.py @@ -3,9 +3,7 @@ def __init__(self, get_response): self.get_response = get_response def __call__(self, request): - if request.method == "GET" and "source" in request.GET: request.session["source"] = request.GET.get("source") - response = self.get_response(request) - return response + return self.get_response(request) diff --git a/electionleaflets/apps/leaflets/migrations/0001_initial.py b/electionleaflets/apps/leaflets/migrations/0001_initial.py index e6c49642..6edac060 100644 --- a/electionleaflets/apps/leaflets/migrations/0001_initial.py +++ b/electionleaflets/apps/leaflets/migrations/0001_initial.py @@ -1,12 +1,11 @@ # Generated by Django 2.2.20 on 2021-04-24 13:06 -from django.db import migrations, models import django.db.models.deletion import sorl.thumbnail.fields +from django.db import migrations, models class Migration(migrations.Migration): - dependencies = [ ("elections", "0001_initial"), ("people", "0001_initial"), @@ -109,7 +108,9 @@ class Migration(migrations.Migration): models.CharField(blank=True, max_length=255, null=True), ), ], - options={"ordering": ("-date_uploaded",),}, + options={ + "ordering": ("-date_uploaded",), + }, ), migrations.CreateModel( name="LeafletImage", @@ -179,6 +180,8 @@ class Migration(migrations.Migration): ), ), ], - options={"ordering": ["image_type"],}, + options={ + "ordering": ["image_type"], + }, ), ] diff --git a/electionleaflets/apps/leaflets/migrations/0002_auto_20210501_1607.py b/electionleaflets/apps/leaflets/migrations/0002_auto_20210501_1607.py index fbd3fb2d..3880d476 100644 --- a/electionleaflets/apps/leaflets/migrations/0002_auto_20210501_1607.py +++ b/electionleaflets/apps/leaflets/migrations/0002_auto_20210501_1607.py @@ -5,7 +5,6 @@ class Migration(migrations.Migration): - dependencies = [ ("leaflets", "0001_initial"), ] diff --git a/electionleaflets/apps/leaflets/migrations/0003_auto_20210501_1655.py b/electionleaflets/apps/leaflets/migrations/0003_auto_20210501_1655.py index 86b5244e..e85147c8 100644 --- a/electionleaflets/apps/leaflets/migrations/0003_auto_20210501_1655.py +++ b/electionleaflets/apps/leaflets/migrations/0003_auto_20210501_1655.py @@ -5,7 +5,6 @@ class Migration(migrations.Migration): - dependencies = [ ("leaflets", "0002_auto_20210501_1607"), ] diff --git a/electionleaflets/apps/leaflets/migrations/0004_auto_20210502_1255.py b/electionleaflets/apps/leaflets/migrations/0004_auto_20210502_1255.py index 0b5395a0..8f5ac9d3 100644 --- a/electionleaflets/apps/leaflets/migrations/0004_auto_20210502_1255.py +++ b/electionleaflets/apps/leaflets/migrations/0004_auto_20210502_1255.py @@ -22,7 +22,6 @@ def change_ballot_data_structure(apps, schema_editor): class Migration(migrations.Migration): - dependencies = [ ("leaflets", "0003_auto_20210501_1655"), ] diff --git a/electionleaflets/apps/leaflets/migrations/0005_auto_20240617_1459.py b/electionleaflets/apps/leaflets/migrations/0005_auto_20240617_1459.py index 47c693ec..bd88f0d2 100644 --- a/electionleaflets/apps/leaflets/migrations/0005_auto_20240617_1459.py +++ b/electionleaflets/apps/leaflets/migrations/0005_auto_20240617_1459.py @@ -4,24 +4,33 @@ class Migration(migrations.Migration): - dependencies = [ - ('leaflets', '0004_auto_20210502_1255'), + ("leaflets", "0004_auto_20210502_1255"), ] operations = [ migrations.AlterModelOptions( - name='leafletimage', - options={'ordering': ['pk', 'image_type']}, + name="leafletimage", + options={"ordering": ["pk", "image_type"]}, ), migrations.AddField( - model_name='leaflet', - name='modified', + model_name="leaflet", + name="modified", field=models.DateTimeField(auto_now=True), ), migrations.AlterField( - model_name='leaflet', - name='status', - field=models.CharField(blank=True, choices=[('live', 'Live'), ('draft', 'Draft'), ('removed', 'Removed')], default='draft', max_length=255, null=True), + model_name="leaflet", + name="status", + field=models.CharField( + blank=True, + choices=[ + ("live", "Live"), + ("draft", "Draft"), + ("removed", "Removed"), + ], + default="draft", + max_length=255, + null=True, + ), ), ] diff --git a/electionleaflets/apps/leaflets/models.py b/electionleaflets/apps/leaflets/models.py index 3d584ff3..08b67af2 100644 --- a/electionleaflets/apps/leaflets/models.py +++ b/electionleaflets/apps/leaflets/models.py @@ -4,25 +4,22 @@ from pathlib import Path import piexif -from django.core.files import File -from django.core.files.storage import default_storage -from slugify import slugify - from constituencies.models import Constituency -from core.helpers import geocode from django.core.files.base import ContentFile +from django.core.files.storage import default_storage from django.db import models from django.db.models import JSONField from django.forms.models import model_to_dict from django.urls import reverse - -from electionleaflets.storages import TempUploadBaseMixin from elections.models import Election from people.models import Person from PIL import Image +from slugify import slugify from sorl.thumbnail import ImageField, delete from uk_political_parties.models import Party +from electionleaflets.storages import TempUploadBaseMixin + from . import constants @@ -104,10 +101,9 @@ def get_first_image(self): def get_title(self): if self.title and len(self.title): return self.title - elif self.publisher_party: + if self.publisher_party: return "%s leaflet" % self.publisher_party.party_name - else: - "Untitled leaflet" + return None def get_person(self): if ( @@ -121,7 +117,7 @@ def get_person(self): ), "name": self.ynr_person_name, } - elif self.publisher_person: + if self.publisher_person: return { "link": reverse( "person", @@ -129,6 +125,7 @@ def get_person(self): ), "name": self.publisher_person.name, } + return None def get_party(self): if self.ynr_party_id and self.ynr_party_name: @@ -137,13 +134,14 @@ def get_party(self): "link": reverse("party-view", kwargs={"pk": pp_id}), "name": self.ynr_party_name, } - elif self.publisher_party: + if self.publisher_party: return { "link": reverse( "party-view", kwargs={"pk": self.publisher_party.pk} ), "name": self.publisher_party.party_name, } + return None class LeafletImage(models.Model): @@ -205,9 +203,9 @@ def _remove_exif_data(self): def _correctly_orient_image(self, image): if self.orientation == 3: return image.transpose(Image.ROTATE_180) - elif self.orientation == 6: + if self.orientation == 6: return image.transpose(Image.ROTATE_270) - elif self.orientation == 8: + if self.orientation == 8: return image.transpose(Image.ROTATE_90) return image @@ -309,13 +307,17 @@ def set_image_from_temp_file(self, temp_file): """ if not self.leaflet_id: - raise ValueError("Parent Leaflet instance needs to be saved " - "before a LeafletImage can be created") + raise ValueError( + "Parent Leaflet instance needs to be saved " + "before a LeafletImage can be created" + ) if not isinstance(default_storage, TempUploadBaseMixin): raise ValueError("Storage class needs to use `TempUploadBaseMixin`") file_name, ext = os.path.basename(temp_file).rsplit(".") - target_file_path = Path(f"leaflets/{self.leaflet.pk}/{slugify(file_name)}.{ext}") + target_file_path = Path( + f"leaflets/{self.leaflet.pk}/{slugify(file_name)}.{ext}" + ) default_storage.save_from_temp_upload(temp_file, target_file_path) self.image.name = str(target_file_path) diff --git a/electionleaflets/apps/leaflets/templatetags/leaflet_tags.py b/electionleaflets/apps/leaflets/templatetags/leaflet_tags.py index 813e9fa5..42ce538a 100644 --- a/electionleaflets/apps/leaflets/templatetags/leaflet_tags.py +++ b/electionleaflets/apps/leaflets/templatetags/leaflet_tags.py @@ -1,7 +1,8 @@ +import os + from django import template from django.conf import settings from leaflets.models import Leaflet -import os register = template.Library() @@ -15,8 +16,7 @@ def leaflet_carousel(): @register.simple_tag def get_medium_image_from_upload(file_path): path = os.path.join(settings.MEDIA_URL, file_path.name) - path = path.replace("uploads/", "uploads/medium/") - return path + return path.replace("uploads/", "uploads/medium/") @register.filter diff --git a/electionleaflets/apps/leaflets/tests/conftest.py b/electionleaflets/apps/leaflets/tests/conftest.py index 5597b579..cbe4a6c7 100644 --- a/electionleaflets/apps/leaflets/tests/conftest.py +++ b/electionleaflets/apps/leaflets/tests/conftest.py @@ -4,13 +4,11 @@ import boto3 import pytest from django.core.files.storage import default_storage - from moto import mock_aws TEST_IMAGE_LOCATION = Path(__file__).parent / "test_images/front_test.jpg" - @pytest.fixture() def uploaded_temp_file(): """ @@ -24,19 +22,20 @@ def uploaded_temp_file(): """ path = "test-leaflet.jpeg" - with default_storage.open(path, 'wb') as f: + with default_storage.open(path, "wb") as f: f.write(TEST_IMAGE_LOCATION.read_bytes()) return path + @pytest.fixture def mock_get_ballot_data_from_ynr(): def _mock_ynr_value(return_value): return patch( "leaflets.forms.PartyForm.get_ballot_data_from_ynr", - return_value=return_value + return_value=return_value, ) - return _mock_ynr_value + return _mock_ynr_value @pytest.fixture @@ -47,7 +46,7 @@ def s3_client(): client = boto3.client("s3") yield client + @pytest.fixture def s3_bucket(s3_client, settings): - bucket = s3_client.create_bucket(Bucket=settings.AWS_STORAGE_BUCKET_NAME) - return bucket + return s3_client.create_bucket(Bucket=settings.AWS_STORAGE_BUCKET_NAME) diff --git a/electionleaflets/apps/leaflets/tests/data.py b/electionleaflets/apps/leaflets/tests/data.py index 9bc2e0de..0bb8efbb 100644 --- a/electionleaflets/apps/leaflets/tests/data.py +++ b/electionleaflets/apps/leaflets/tests/data.py @@ -2,10 +2,10 @@ {"wgs84_lat": 51.4599323104553, "coordsyst": "G", "shortcuts": {"WMC": 65913, "ward": 8323, "council": 2491}, "wgs84_lon": -0.0824797738988752, "postcode": "SE22 8DJ", "easting": 533310, "areas": {"900000": {"parent_area": null, "generation_high": 19, "all_names": {}, "id": 900000, "codes": {}, "name": "House of Commons", "country": "", "type_name": "UK Parliament", "generation_low": 1, "country_name": "-", "type": "WMP"}, "900001": {"parent_area": null, "generation_high": 23, "all_names": {}, "id": 900001, "codes": {}, "name": "European Parliament", "country": "", "type_name": "European Parliament", "generation_low": 1, "country_name": "-", "type": "EUP"}, "900002": {"parent_area": 900006, "generation_high": 23, "all_names": {}, "id": 900002, "codes": {}, "name": "London Assembly", "country": "E", "type_name": "London Assembly area (shared)", "generation_low": 1, "country_name": "England", "type": "LAE"}, "8323": {"parent_area": 2491, "generation_high": 23, "all_names": {}, "id": 8323, "codes": {"ons": "00BEGW", "gss": "E05000551", "unit_id": "11015"}, "name": "South Camberwell", "country": "E", "type_name": "London borough ward", "generation_low": 1, "country_name": "England", "type": "LBW"}, "900006": {"parent_area": null, "generation_high": 23, "all_names": {}, "id": 900006, "codes": {}, "name": "London Assembly", "country": "E", "type_name": "London Assembly area", "generation_low": 1, "country_name": "England", "type": "LAS"}, "2247": {"parent_area": null, "generation_high": 23, "all_names": {}, "id": 2247, "codes": {"unit_id": "41441"}, "name": "Greater London Authority", "country": "E", "type_name": "Greater London Authority", "generation_low": 1, "country_name": "England", "type": "GLA"}, "70189": {"parent_area": null, "generation_high": 23, "all_names": {}, "id": 70189, "codes": {"ons": "E01004048"}, "name": "Southwark 027C", "country": "E", "type_name": "Lower Layer Super Output Area (Full)", "generation_low": 13, "country_name": "England", "type": "OLF"}, "11822": {"parent_area": 2247, "generation_high": 23, "all_names": {}, "id": 11822, "codes": {"gss": "E32000010", "unit_id": "41446"}, "name": "Lambeth and Southwark", "country": "E", "type_name": "London Assembly constituency", "generation_low": 1, "country_name": "England", "type": "LAC"}, "41906": {"parent_area": null, "generation_high": 23, "all_names": {}, "id": 41906, "codes": {"ons": "E02000833"}, "name": "Southwark 027", "country": "E", "type_name": "Middle Layer Super Output Area (Generalised)", "generation_low": 13, "country_name": "England", "type": "OMG"}, "104567": {"parent_area": null, "generation_high": 23, "all_names": {}, "id": 104567, "codes": {"ons": "E01004048"}, "name": "Southwark 027C", "country": "E", "type_name": "Lower Layer Super Output Area (Generalised)", "generation_low": 13, "country_name": "England", "type": "OLG"}, "34712": {"parent_area": null, "generation_high": 23, "all_names": {}, "id": 34712, "codes": {"ons": "E02000833"}, "name": "Southwark 027", "country": "E", "type_name": "Middle Layer Super Output Area (Full)", "generation_low": 13, "country_name": "England", "type": "OMF"}, "65913": {"parent_area": null, "generation_high": 23, "all_names": {}, "id": 65913, "codes": {"gss": "E14000615", "unit_id": "25066"}, "name": "Camberwell and Peckham", "country": "E", "type_name": "UK Parliament constituency", "generation_low": 13, "country_name": "England", "type": "WMC"}, "2491": {"parent_area": null, "generation_high": 23, "all_names": {}, "id": 2491, "codes": {"ons": "00BE", "gss": "E09000028", "unit_id": "11013"}, "name": "Southwark Borough Council", "country": "E", "type_name": "London borough", "generation_low": 1, "country_name": "England", "type": "LBO"}, "11806": {"parent_area": null, "generation_high": 23, "all_names": {}, "id": 11806, "codes": {"ons": "07", "gss": "E15000007", "unit_id": "41428"}, "name": "London", "country": "E", "type_name": "European region", "generation_low": 1, "country_name": "England", "type": "EUR"}}, "northing": 175189}""" LOCAL_BALLOT_WITH_CANDIDATES = { - "url": "https://candidates.democracyclub.org.uk/api/next/ballots/local.westminster.st-jamess.2022-05-05/", - "history_url": "https://candidates.democracyclub.org.uk/api/next/ballots/local.westminster.st-jamess.2022-05-05/history/", - "results_url": "https://candidates.democracyclub.org.uk/api/next/results/local.westminster.st-jamess.2022-05-05/", - "election": { + "url": "https://candidates.democracyclub.org.uk/api/next/ballots/local.westminster.st-jamess.2022-05-05/", + "history_url": "https://candidates.democracyclub.org.uk/api/next/ballots/local.westminster.st-jamess.2022-05-05/history/", + "results_url": "https://candidates.democracyclub.org.uk/api/next/results/local.westminster.st-jamess.2022-05-05/", + "election": { "election_id": "local.westminster.2022-05-05", "url": "https://candidates.democracyclub.org.uk/api/next/elections/local.westminster.2022-05-05/", "name": "Westminster local election", @@ -13,279 +13,252 @@ "current": False, "party_lists_in_use": False, "created": "2022-01-05T11:15:54.488413Z", - "last_updated": "2024-01-16T18:09:16.371357Z" - }, - "post": { + "last_updated": "2024-01-16T18:09:16.371357Z", + }, + "post": { "id": "gss:E05013806", "label": "St James's", "slug": "st-jamess", "created": "2022-01-05T11:15:54.795194Z", - "last_updated": "2023-07-11T15:31:13.176989+01:00" - }, - "winner_count": 3, - "ballot_paper_id": "local.westminster.st-jamess.2022-05-05", - "cancelled": False, - "sopn": { + "last_updated": "2023-07-11T15:31:13.176989+01:00", + }, + "winner_count": 3, + "ballot_paper_id": "local.westminster.st-jamess.2022-05-05", + "cancelled": False, + "sopn": { "uploaded_file": "https://s3.eu-west-2.amazonaws.com/static-candidates.democracyclub.org.uk/media/official_documents/local.westminster.st-jamess.2022-05-05/statement-of-persons-nominated.pdf", - "source_url": "https://www.westminster.gov.uk/media/document/st-jamess-ward-statement-of-persons-nominated" - }, - "candidates_locked": True, - "candidacies": [ + "source_url": "https://www.westminster.gov.uk/media/document/st-jamess-ward-statement-of-persons-nominated", + }, + "candidates_locked": True, + "candidacies": [ { - "elected": True, - "party_list_position": None, - "party": { - "url": "https://candidates.democracyclub.org.uk/api/next/parties/PP52/", - "ec_id": "PP52", - "name": "Conservative and Unionist Party", - "legacy_slug": "party:52", - "created": "2018-09-17T15:01:08.048032+01:00", - "modified": "2024-11-19T02:06:24.008964Z" - }, - "party_name": "Conservative and Unionist Party", - "party_description_text": "", - "deselected": False, - "deselected_source": "", - "created": "2022-04-06T15:06:56.273587+01:00", - "modified": "2022-05-06T04:12:08.754253+01:00", - "person": { - "id": 41112, - "url": "https://candidates.democracyclub.org.uk/api/next/people/41112/", - "name": "Louise Hyams" - }, - "result": { "elected": True, - "num_ballots": 979 - } + "party_list_position": None, + "party": { + "url": "https://candidates.democracyclub.org.uk/api/next/parties/PP52/", + "ec_id": "PP52", + "name": "Conservative and Unionist Party", + "legacy_slug": "party:52", + "created": "2018-09-17T15:01:08.048032+01:00", + "modified": "2024-11-19T02:06:24.008964Z", + }, + "party_name": "Conservative and Unionist Party", + "party_description_text": "", + "deselected": False, + "deselected_source": "", + "created": "2022-04-06T15:06:56.273587+01:00", + "modified": "2022-05-06T04:12:08.754253+01:00", + "person": { + "id": 41112, + "url": "https://candidates.democracyclub.org.uk/api/next/people/41112/", + "name": "Louise Hyams", + }, + "result": {"elected": True, "num_ballots": 979}, }, { - "elected": True, - "party_list_position": None, - "party": { - "url": "https://candidates.democracyclub.org.uk/api/next/parties/PP52/", - "ec_id": "PP52", - "name": "Conservative and Unionist Party", - "legacy_slug": "party:52", - "created": "2018-09-17T15:01:08.048032+01:00", - "modified": "2024-11-19T02:06:24.008964Z" - }, - "party_name": "Conservative and Unionist Party", - "party_description_text": "", - "deselected": False, - "deselected_source": "", - "created": "2022-04-06T15:06:56.327388+01:00", - "modified": "2022-05-06T04:12:08.836342+01:00", - "person": { - "id": 41114, - "url": "https://candidates.democracyclub.org.uk/api/next/people/41114/", - "name": "Tim Mitchell" - }, - "result": { "elected": True, - "num_ballots": 965 - } + "party_list_position": None, + "party": { + "url": "https://candidates.democracyclub.org.uk/api/next/parties/PP52/", + "ec_id": "PP52", + "name": "Conservative and Unionist Party", + "legacy_slug": "party:52", + "created": "2018-09-17T15:01:08.048032+01:00", + "modified": "2024-11-19T02:06:24.008964Z", + }, + "party_name": "Conservative and Unionist Party", + "party_description_text": "", + "deselected": False, + "deselected_source": "", + "created": "2022-04-06T15:06:56.327388+01:00", + "modified": "2022-05-06T04:12:08.836342+01:00", + "person": { + "id": 41114, + "url": "https://candidates.democracyclub.org.uk/api/next/people/41114/", + "name": "Tim Mitchell", + }, + "result": {"elected": True, "num_ballots": 965}, }, { - "elected": True, - "party_list_position": None, - "party": { - "url": "https://candidates.democracyclub.org.uk/api/next/parties/PP52/", - "ec_id": "PP52", - "name": "Conservative and Unionist Party", - "legacy_slug": "party:52", - "created": "2018-09-17T15:01:08.048032+01:00", - "modified": "2024-11-19T02:06:24.008964Z" - }, - "party_name": "Conservative and Unionist Party", - "party_description_text": "", - "deselected": False, - "deselected_source": "", - "created": "2022-04-06T15:06:56.389619+01:00", - "modified": "2022-05-06T04:12:08.912046+01:00", - "person": { - "id": 41117, - "url": "https://candidates.democracyclub.org.uk/api/next/people/41117/", - "name": "Mark Shearer" - }, - "result": { "elected": True, - "num_ballots": 954 - } + "party_list_position": None, + "party": { + "url": "https://candidates.democracyclub.org.uk/api/next/parties/PP52/", + "ec_id": "PP52", + "name": "Conservative and Unionist Party", + "legacy_slug": "party:52", + "created": "2018-09-17T15:01:08.048032+01:00", + "modified": "2024-11-19T02:06:24.008964Z", + }, + "party_name": "Conservative and Unionist Party", + "party_description_text": "", + "deselected": False, + "deselected_source": "", + "created": "2022-04-06T15:06:56.389619+01:00", + "modified": "2022-05-06T04:12:08.912046+01:00", + "person": { + "id": 41117, + "url": "https://candidates.democracyclub.org.uk/api/next/people/41117/", + "name": "Mark Shearer", + }, + "result": {"elected": True, "num_ballots": 954}, }, { - "elected": False, - "party_list_position": None, - "party": { - "url": "https://candidates.democracyclub.org.uk/api/next/parties/PP53/", - "ec_id": "PP53", - "name": "Labour Party", - "legacy_slug": "party:53", - "created": "2018-09-17T15:01:09.580623+01:00", - "modified": "2024-11-15T02:06:31.822331Z" - }, - "party_name": "Labour Party", - "party_description_text": "", - "deselected": False, - "deselected_source": "", - "created": "2022-04-07T16:50:03.294367+01:00", - "modified": "2022-04-07T16:50:03.296651+01:00", - "person": { - "id": 92971, - "url": "https://candidates.democracyclub.org.uk/api/next/people/92971/", - "name": "Karina Darbin" - }, - "result": { "elected": False, - "num_ballots": 789 - } + "party_list_position": None, + "party": { + "url": "https://candidates.democracyclub.org.uk/api/next/parties/PP53/", + "ec_id": "PP53", + "name": "Labour Party", + "legacy_slug": "party:53", + "created": "2018-09-17T15:01:09.580623+01:00", + "modified": "2024-11-15T02:06:31.822331Z", + }, + "party_name": "Labour Party", + "party_description_text": "", + "deselected": False, + "deselected_source": "", + "created": "2022-04-07T16:50:03.294367+01:00", + "modified": "2022-04-07T16:50:03.296651+01:00", + "person": { + "id": 92971, + "url": "https://candidates.democracyclub.org.uk/api/next/people/92971/", + "name": "Karina Darbin", + }, + "result": {"elected": False, "num_ballots": 789}, }, { - "elected": False, - "party_list_position": None, - "party": { - "url": "https://candidates.democracyclub.org.uk/api/next/parties/PP53/", - "ec_id": "PP53", - "name": "Labour Party", - "legacy_slug": "party:53", - "created": "2018-09-17T15:01:09.580623+01:00", - "modified": "2024-11-15T02:06:31.822331Z" - }, - "party_name": "Labour Party", - "party_description_text": "", - "deselected": False, - "deselected_source": "", - "created": "2022-04-07T16:50:03.479288+01:00", - "modified": "2022-04-07T16:50:03.481537+01:00", - "person": { - "id": 92973, - "url": "https://candidates.democracyclub.org.uk/api/next/people/92973/", - "name": "Paul Raphael James Spence" - }, - "result": { "elected": False, - "num_ballots": 701 - } + "party_list_position": None, + "party": { + "url": "https://candidates.democracyclub.org.uk/api/next/parties/PP53/", + "ec_id": "PP53", + "name": "Labour Party", + "legacy_slug": "party:53", + "created": "2018-09-17T15:01:09.580623+01:00", + "modified": "2024-11-15T02:06:31.822331Z", + }, + "party_name": "Labour Party", + "party_description_text": "", + "deselected": False, + "deselected_source": "", + "created": "2022-04-07T16:50:03.479288+01:00", + "modified": "2022-04-07T16:50:03.481537+01:00", + "person": { + "id": 92973, + "url": "https://candidates.democracyclub.org.uk/api/next/people/92973/", + "name": "Paul Raphael James Spence", + }, + "result": {"elected": False, "num_ballots": 701}, }, { - "elected": False, - "party_list_position": None, - "party": { - "url": "https://candidates.democracyclub.org.uk/api/next/parties/PP53/", - "ec_id": "PP53", - "name": "Labour Party", - "legacy_slug": "party:53", - "created": "2018-09-17T15:01:09.580623+01:00", - "modified": "2024-11-15T02:06:31.822331Z" - }, - "party_name": "Labour Party", - "party_description_text": "", - "deselected": False, - "deselected_source": "", - "created": "2022-04-07T16:50:03.416071+01:00", - "modified": "2022-04-07T16:50:03.418536+01:00", - "person": { - "id": 92972, - "url": "https://candidates.democracyclub.org.uk/api/next/people/92972/", - "name": "Nigel Stephen Medforth" - }, - "result": { "elected": False, - "num_ballots": 700 - } + "party_list_position": None, + "party": { + "url": "https://candidates.democracyclub.org.uk/api/next/parties/PP53/", + "ec_id": "PP53", + "name": "Labour Party", + "legacy_slug": "party:53", + "created": "2018-09-17T15:01:09.580623+01:00", + "modified": "2024-11-15T02:06:31.822331Z", + }, + "party_name": "Labour Party", + "party_description_text": "", + "deselected": False, + "deselected_source": "", + "created": "2022-04-07T16:50:03.416071+01:00", + "modified": "2022-04-07T16:50:03.418536+01:00", + "person": { + "id": 92972, + "url": "https://candidates.democracyclub.org.uk/api/next/people/92972/", + "name": "Nigel Stephen Medforth", + }, + "result": {"elected": False, "num_ballots": 700}, }, { - "elected": False, - "party_list_position": None, - "party": { - "url": "https://candidates.democracyclub.org.uk/api/next/parties/PP90/", - "ec_id": "PP90", - "name": "Liberal Democrats", - "legacy_slug": "party:90", - "created": "2018-09-17T15:01:08.091504+01:00", - "modified": "2024-11-19T02:06:24.311670Z" - }, - "party_name": "Liberal Democrats", - "party_description_text": "", - "deselected": False, - "deselected_source": "", - "created": "2022-04-07T16:50:03.221833+01:00", - "modified": "2022-04-07T16:50:03.224684+01:00", - "person": { - "id": 92970, - "url": "https://candidates.democracyclub.org.uk/api/next/people/92970/", - "name": "Michael Anthony Ahearne" - }, - "result": { "elected": False, - "num_ballots": 295 - } + "party_list_position": None, + "party": { + "url": "https://candidates.democracyclub.org.uk/api/next/parties/PP90/", + "ec_id": "PP90", + "name": "Liberal Democrats", + "legacy_slug": "party:90", + "created": "2018-09-17T15:01:08.091504+01:00", + "modified": "2024-11-19T02:06:24.311670Z", + }, + "party_name": "Liberal Democrats", + "party_description_text": "", + "deselected": False, + "deselected_source": "", + "created": "2022-04-07T16:50:03.221833+01:00", + "modified": "2022-04-07T16:50:03.224684+01:00", + "person": { + "id": 92970, + "url": "https://candidates.democracyclub.org.uk/api/next/people/92970/", + "name": "Michael Anthony Ahearne", + }, + "result": {"elected": False, "num_ballots": 295}, }, { - "elected": False, - "party_list_position": None, - "party": { - "url": "https://candidates.democracyclub.org.uk/api/next/parties/PP90/", - "ec_id": "PP90", - "name": "Liberal Democrats", - "legacy_slug": "party:90", - "created": "2018-09-17T15:01:08.091504+01:00", - "modified": "2024-11-19T02:06:24.311670Z" - }, - "party_name": "Liberal Democrats", - "party_description_text": "", - "deselected": False, - "deselected_source": "", - "created": "2022-04-07T16:50:03.341602+01:00", - "modified": "2022-04-07T16:50:03.343789+01:00", - "person": { - "id": 41110, - "url": "https://candidates.democracyclub.org.uk/api/next/people/41110/", - "name": "Paul Diggory" - }, - "result": { "elected": False, - "num_ballots": 281 - } + "party_list_position": None, + "party": { + "url": "https://candidates.democracyclub.org.uk/api/next/parties/PP90/", + "ec_id": "PP90", + "name": "Liberal Democrats", + "legacy_slug": "party:90", + "created": "2018-09-17T15:01:08.091504+01:00", + "modified": "2024-11-19T02:06:24.311670Z", + }, + "party_name": "Liberal Democrats", + "party_description_text": "", + "deselected": False, + "deselected_source": "", + "created": "2022-04-07T16:50:03.341602+01:00", + "modified": "2022-04-07T16:50:03.343789+01:00", + "person": { + "id": 41110, + "url": "https://candidates.democracyclub.org.uk/api/next/people/41110/", + "name": "Paul Diggory", + }, + "result": {"elected": False, "num_ballots": 281}, }, { - "elected": False, - "party_list_position": None, - "party": { - "url": "https://candidates.democracyclub.org.uk/api/next/parties/PP90/", - "ec_id": "PP90", - "name": "Liberal Democrats", - "legacy_slug": "party:90", - "created": "2018-09-17T15:01:08.091504+01:00", - "modified": "2024-11-19T02:06:24.311670Z" - }, - "party_name": "Liberal Democrats", - "party_description_text": "", - "deselected": False, - "deselected_source": "", - "created": "2022-04-07T16:50:03.544670+01:00", - "modified": "2022-04-07T16:50:03.546973+01:00", - "person": { - "id": 92974, - "url": "https://candidates.democracyclub.org.uk/api/next/people/92974/", - "name": "Alice Anne Wells" - }, - "result": { "elected": False, - "num_ballots": 249 - } - } - ], - "created": "2022-01-05T11:15:54.801506Z", - "last_updated": "2024-01-16T18:09:16.478549Z", - "replaces": None, - "replaced_by": None, - "uncontested": False, - "results": { + "party_list_position": None, + "party": { + "url": "https://candidates.democracyclub.org.uk/api/next/parties/PP90/", + "ec_id": "PP90", + "name": "Liberal Democrats", + "legacy_slug": "party:90", + "created": "2018-09-17T15:01:08.091504+01:00", + "modified": "2024-11-19T02:06:24.311670Z", + }, + "party_name": "Liberal Democrats", + "party_description_text": "", + "deselected": False, + "deselected_source": "", + "created": "2022-04-07T16:50:03.544670+01:00", + "modified": "2022-04-07T16:50:03.546973+01:00", + "person": { + "id": 92974, + "url": "https://candidates.democracyclub.org.uk/api/next/people/92974/", + "name": "Alice Anne Wells", + }, + "result": {"elected": False, "num_ballots": 249}, + }, + ], + "created": "2022-01-05T11:15:54.801506Z", + "last_updated": "2024-01-16T18:09:16.478549Z", + "replaces": None, + "replaced_by": None, + "uncontested": False, + "results": { "num_turnout_reported": 2057, "turnout_percentage": 30, "num_spoilt_ballots": None, "source": "https://www.westminster.gov.uk/about-council/democracy/elections-referendums-and-how-vote/local-elections-5-may-2022/st-jamess", - "total_electorate": 6943 - }, - "voting_system": "FPTP" - } + "total_electorate": 6943, + }, + "voting_system": "FPTP", +} diff --git a/electionleaflets/apps/leaflets/tests/helpers.py b/electionleaflets/apps/leaflets/tests/helpers.py index 684d968b..24b33f50 100644 --- a/electionleaflets/apps/leaflets/tests/helpers.py +++ b/electionleaflets/apps/leaflets/tests/helpers.py @@ -1,14 +1,13 @@ from django.core.files import File - from leaflets.models import Leaflet, LeafletImage from leaflets.tests.conftest import TEST_IMAGE_LOCATION def create_dummy_leaflets(number=1): for i in range(number): - l = Leaflet() - l.save() + leaflet = Leaflet() + leaflet.save() - li = LeafletImage(leaflet=l) + li = LeafletImage(leaflet=leaflet) li.image.save("test_1.jpg", File(TEST_IMAGE_LOCATION.open("rb"))) li.save() diff --git a/electionleaflets/apps/leaflets/tests/model_factory.py b/electionleaflets/apps/leaflets/tests/model_factory.py index 177935ed..f2503f30 100644 --- a/electionleaflets/apps/leaflets/tests/model_factory.py +++ b/electionleaflets/apps/leaflets/tests/model_factory.py @@ -1,6 +1,5 @@ import factory from factory.django import DjangoModelFactory - from leaflets.models import Leaflet diff --git a/electionleaflets/apps/leaflets/tests/test_frontend.py b/electionleaflets/apps/leaflets/tests/test_frontend.py index 0eb37002..5cd90c52 100644 --- a/electionleaflets/apps/leaflets/tests/test_frontend.py +++ b/electionleaflets/apps/leaflets/tests/test_frontend.py @@ -1,9 +1,10 @@ -import pytest from pathlib import Path -from electionleaflets import settings -from playwright.sync_api import Page, expect +import pytest from leaflets.tests.data import LOCAL_BALLOT_WITH_CANDIDATES +from playwright.sync_api import Page, expect + +from electionleaflets import settings def console_handler(message): @@ -21,12 +22,16 @@ def console_handler(message): return if "net::ERR_INTERNET_DISCONNECTED" in message.text: return - assert not message.text, f"Found browser console output: {message.text}: {message.location}" + assert ( + not message.text + ), f"Found browser console output: {message.text}: {message.location}" -class TestLeafletUpload(): +class TestLeafletUpload: @pytest.fixture(autouse=True) - def setup_method(self, page: Page, live_server, mock_get_ballot_data_from_ynr): + def setup_method( + self, page: Page, live_server, mock_get_ballot_data_from_ynr + ): self.page = page # Raise if the console contains errors self.page.on("console", console_handler) @@ -34,18 +39,21 @@ def setup_method(self, page: Page, live_server, mock_get_ballot_data_from_ynr): self.mock_get_ballot_data_from_ynr = mock_get_ballot_data_from_ynr def navigate_to_home_page(self): - self.page.goto(self.live_server.url) - def get_test_image(self, leaflet_file_path="apps/leaflets/tests/test_images/front_test.jpg"): + def get_test_image( + self, leaflet_file_path="apps/leaflets/tests/test_images/front_test.jpg" + ): project_root = Path(settings.PROJECT_ROOT).resolve() return [str(project_root / leaflet_file_path)] def navigate_to_upload_page(self): - upload_link = self.page.get_by_role('link', name='Upload a leaflet') + upload_link = self.page.get_by_role("link", name="Upload a leaflet") upload_link.click() - expect(self.page).to_have_url(f'{self.live_server.url}/leaflets/add/images/') - take_photo_link = self.page.get_by_label('Take a photo of a leaflet') + expect(self.page).to_have_url( + f"{self.live_server.url}/leaflets/add/images/" + ) + take_photo_link = self.page.get_by_label("Take a photo of a leaflet") take_photo_link.click() def upload_leaflet(self): @@ -53,45 +61,45 @@ def upload_leaflet(self): self.navigate_to_upload_page() files = self.get_test_image() self.page.set_input_files(selector='input[type="file"]', files=files) - submit_button = self.page.get_by_role('button', name='Continue') + submit_button = self.page.get_by_role("button", name="Continue") submit_button.click() def upload_multiple_files_for_one_leaflet(self): self.navigate_to_home_page() self.navigate_to_upload_page() files = self.get_test_image() - self.page.set_input_files(selector='input[type="file"]', files=files) - add_another_image = self.page.get_by_text('Add another image') + self.page.set_input_files(selector='input[type="file"]', files=files) + add_another_image = self.page.get_by_text("Add another image") add_another_image.click() self.page.set_input_files(selector='input[type="file"]', files=files) - submit_button = self.page.get_by_role('button', name='Continue') + submit_button = self.page.get_by_role("button", name="Continue") submit_button.click() - def fill_postcode(self, postcode='SW1A 1AA'): - postcode_input = self.page.get_by_label('What postcode was this') + def fill_postcode(self, postcode="SW1A 1AA"): + postcode_input = self.page.get_by_label("What postcode was this") postcode_input.click() postcode_input.fill(postcode) def select_time_and_submit(self): - time_input = self.page.get_by_text('In the last couple of weeks') + time_input = self.page.get_by_text("In the last couple of weeks") time_input.check() - submit_button = self.page.get_by_role('button', name='Submit') + submit_button = self.page.get_by_role("button", name="Submit") submit_button.click() - def select_party_and_submit(self, party='Green Party'): + def select_party_and_submit(self, party="Green Party"): party_option = self.page.get_by_text(party) party_option.click() - submit_button = self.page.get_by_role('button', name='Submit') + submit_button = self.page.get_by_role("button", name="Submit") submit_button.click() def enter_dates_and_submit(self): - day_input = self.page.get_by_label('day') - day_input.fill('01') - month_input = self.page.get_by_label('month') - month_input.fill('01') - year_input = self.page.get_by_label('year') - year_input.fill('2021') - submit_button = self.page.get_by_role('button', name='Submit') + day_input = self.page.get_by_label("day") + day_input.fill("01") + month_input = self.page.get_by_label("month") + month_input.fill("01") + year_input = self.page.get_by_label("year") + year_input.fill("2021") + submit_button = self.page.get_by_role("button", name="Submit") submit_button.click() def test_basic_upload(self): @@ -101,29 +109,30 @@ def test_basic_upload(self): with self.mock_get_ballot_data_from_ynr([LOCAL_BALLOT_WITH_CANDIDATES]): self.select_time_and_submit() self.select_party_and_submit() - id = self.page.url.split('/')[-2] - expect(self.page).to_have_url(f'{self.live_server.url}/leaflets/{id}/') - expect(self.page.locator('h1')).to_have_text(f'Leaflet #{id}') - expect(self.page.locator('h2')).to_have_text('Leaflet details') + id = self.page.url.split("/")[-2] + expect(self.page).to_have_url(f"{self.live_server.url}/leaflets/{id}/") + expect(self.page.locator("h1")).to_have_text(f"Leaflet #{id}") + expect(self.page.locator("h2")).to_have_text("Leaflet details") def test_upload_leaflet_with_invalid_postcode(self): self.upload_leaflet() - self.fill_postcode(postcode='INVALID') + self.fill_postcode(postcode="INVALID") self.select_time_and_submit() - submit_button = self.page.get_by_role('button', name='Submit') + submit_button = self.page.get_by_role("button", name="Submit") submit_button.click() - error_message = self.page.get_by_text('Please enter a full UK postcode') + error_message = self.page.get_by_text("Please enter a full UK postcode") assert error_message is not None - def test_party_page_content(self): self.upload_leaflet() self.fill_postcode() with self.mock_get_ballot_data_from_ynr([LOCAL_BALLOT_WITH_CANDIDATES]): self.select_time_and_submit() self.select_party_and_submit() - id = self.page.url.split('/')[-2] - expect(self.page).to_have_url(f'{self.live_server.url}/leaflets/{id}/') - labour_party_link = self.page.get_by_role('link', name='Green Party') + id = self.page.url.split("/")[-2] + expect(self.page).to_have_url(f"{self.live_server.url}/leaflets/{id}/") + labour_party_link = self.page.get_by_role("link", name="Green Party") labour_party_link.click() - expect(self.page.locator('h1')).to_have_text('Election leaflets from Green Party') + expect(self.page.locator("h1")).to_have_text( + "Election leaflets from Green Party" + ) diff --git a/electionleaflets/apps/leaflets/tests/test_image_editing.py b/electionleaflets/apps/leaflets/tests/test_image_editing.py index ed58efd3..292505f2 100644 --- a/electionleaflets/apps/leaflets/tests/test_image_editing.py +++ b/electionleaflets/apps/leaflets/tests/test_image_editing.py @@ -1,9 +1,11 @@ import os import shutil + import pytest +from leaflets.models import LeafletImage from electionleaflets import settings -from leaflets.models import Leaflet, LeafletImage + from .helpers import create_dummy_leaflets @@ -12,11 +14,12 @@ def leaflet_image(): create_dummy_leaflets() return LeafletImage.objects.last() + @pytest.mark.django_db class TestImageEditing: def test_dimensions(self, leaflet_image): assert leaflet_image.dimensions == (419, 300) - + def test_crop(self, leaflet_image): assert leaflet_image.dimensions == (419, 300) x = 12 @@ -25,7 +28,7 @@ def test_crop(self, leaflet_image): y2 = 100 leaflet_image.crop(x, y, x2, y2) assert leaflet_image.dimensions == (88, 93) - + def tearDown(self): """ Remove testing files after tests are run diff --git a/electionleaflets/apps/leaflets/tests/test_models.py b/electionleaflets/apps/leaflets/tests/test_models.py index 94174df5..21a9efaf 100644 --- a/electionleaflets/apps/leaflets/tests/test_models.py +++ b/electionleaflets/apps/leaflets/tests/test_models.py @@ -2,47 +2,76 @@ import pytest from leaflets.models import Leaflet, LeafletImage -from uk_political_parties.models import Party - from leaflets.tests.conftest import TEST_IMAGE_LOCATION +from uk_political_parties.models import Party @pytest.fixture def leaflet(): return Leaflet.objects.create(title="Test Leaflet", description=None) + @pytest.fixture def party(): return Party.objects.create(party_name="Labour Party") + @pytest.mark.django_db def test_model_initial(): leaflet = Leaflet() assert leaflet._initial == { - 'id': None, 'title': '', 'description': None, 'publisher_party': None, 'ynr_party_id': None, 'ynr_party_name': None, 'publisher_person': None, 'ynr_person_id': None, 'ynr_person_name': None, 'ballot_id': None, 'ballots': [], 'people': {}, 'person_ids': [], 'election': None, 'constituency': None, 'imprint': None, 'postcode': '', 'name': '', 'email': '', 'date_delivered': None, 'status': 'draft', 'reviewed': False + "id": None, + "title": "", + "description": None, + "publisher_party": None, + "ynr_party_id": None, + "ynr_party_name": None, + "publisher_person": None, + "ynr_person_id": None, + "ynr_person_name": None, + "ballot_id": None, + "ballots": [], + "people": {}, + "person_ids": [], + "election": None, + "constituency": None, + "imprint": None, + "postcode": "", + "name": "", + "email": "", + "date_delivered": None, + "status": "draft", + "reviewed": False, } assert leaflet._initial["status"] == "draft" + @pytest.mark.django_db def test_markdown_error(client, leaflet): response = client.get(leaflet.get_absolute_url()) assert response.status_code == 200 + @pytest.mark.django_db def test_leaflet_detail(client, party): leaflet = Leaflet.objects.create( - title="Test Leaflet", description=None, ynr_party_id="party:1", ynr_party_name="Labour Party", publisher_party=party + title="Test Leaflet", + description=None, + ynr_party_id="party:1", + ynr_party_name="Labour Party", + publisher_party=party, ) response = client.get(leaflet.get_absolute_url()) assert response.status_code == 200 assert "Test Leaflet" in response.content.decode() assert "Labour Party" in response.content.decode() + @pytest.mark.django_db def test_raw_image_field(): - l = Leaflet() - l.save() - li = LeafletImage(leaflet=l) + leaflet = Leaflet() + leaflet.save() + li = LeafletImage(leaflet=leaflet) assert li.raw_image.name == "" with TEST_IMAGE_LOCATION.open("rb") as img_file: li.image.save("front_test.jpg", img_file) @@ -50,12 +79,13 @@ def test_raw_image_field(): def test_save_leaflet_image_from_temp_file(db): - leaflet_image = LeafletImage() with pytest.raises(ValueError) as e_info: - leaflet_image.set_image_from_temp_file("/not/a/path") - assert str(e_info.value) == ("Parent Leaflet instance needs to be saved " - "before a LeafletImage can be created") + leaflet_image.set_image_from_temp_file("/not/a/path") + assert str(e_info.value) == ( + "Parent Leaflet instance needs to be saved " + "before a LeafletImage can be created" + ) def test_image_moved_from_temp_upload(db, uploaded_temp_file): @@ -67,21 +97,35 @@ def test_image_moved_from_temp_upload(db, uploaded_temp_file): leaflet_image.save() leaflet_image.refresh_from_db() - path = Path(leaflet_image.image.storage.base_location) / f"leaflets/{leaflet.pk}/test-leaflet.jpeg" + path = ( + Path(leaflet_image.image.storage.base_location) + / f"leaflets/{leaflet.pk}/test-leaflet.jpeg" + ) assert str(path) == leaflet_image.image.path -def test_image_moved_from_temp_upload_s3_backend(db, settings, s3_bucket, s3_client): - settings.DEFAULT_FILE_STORAGE = "electionleaflets.storages.TempUploadS3MediaStorage" + +def test_image_moved_from_temp_upload_s3_backend( + db, settings, s3_bucket, s3_client +): + settings.DEFAULT_FILE_STORAGE = ( + "electionleaflets.storages.TempUploadS3MediaStorage" + ) leaflet = Leaflet() leaflet.save() leaflet.refresh_from_db() leaflet_image = LeafletImage(leaflet=leaflet) - s3_client.put_object(Key="test_images/test_leaflet.jpeg", Body="", Bucket=settings.AWS_STORAGE_BUCKET_NAME) + s3_client.put_object( + Key="test_images/test_leaflet.jpeg", + Body="", + Bucket=settings.AWS_STORAGE_BUCKET_NAME, + ) leaflet_image.set_image_from_temp_file("test_images/test_leaflet.jpeg") leaflet_image.save() leaflet_image.refresh_from_db() key = f"leaflets/{leaflet.pk}/test-leaflet.jpeg" - assert s3_client.head_object(Bucket=settings.AWS_STORAGE_BUCKET_NAME, Key=key) + assert s3_client.head_object( + Bucket=settings.AWS_STORAGE_BUCKET_NAME, Key=key + ) assert str(key) == leaflet_image.image.file.name diff --git a/electionleaflets/apps/leaflets/urls.py b/electionleaflets/apps/leaflets/urls.py index ca671c13..41d68fef 100644 --- a/electionleaflets/apps/leaflets/urls.py +++ b/electionleaflets/apps/leaflets/urls.py @@ -1,12 +1,20 @@ from django.contrib.auth.decorators import login_required from django.urls import re_path from django.views.decorators.cache import never_cache -from leaflets.views import (AllImageView, ImageCropView, ImageRotateView, - ImageView, LatestLeaflets, LeafletModeration, - LeafletUpdatePublisherView, LeafletUploadWizzard, - LeafletView, LegacyImageView, - should_show_date_form, should_show_party_form, - should_show_person_form) +from leaflets.views import ( + AllImageView, + ImageCropView, + ImageRotateView, + ImageView, + LatestLeaflets, + LeafletModeration, + LeafletUpdatePublisherView, + LeafletUploadWizzard, + LeafletView, + LegacyImageView, + should_show_date_form, + should_show_person_form, +) from .forms import DateForm, ImagesForm, PartyForm, PeopleForm, PostcodeForm @@ -41,7 +49,9 @@ LegacyImageView.as_view(), name="full_image_legacy", ), - re_path(r"^(?P\d+)/images/$", AllImageView.as_view(), name="all_images"), + re_path( + r"^(?P\d+)/images/$", AllImageView.as_view(), name="all_images" + ), re_path(r"^crop/(?P.+)/$", ImageCropView.as_view(), name="crop"), re_path(r"^rotate/(?P.+)/$", ImageRotateView.as_view(), name="rotate"), re_path(r"^(?P\d+)/$", LeafletView.as_view(), name="leaflet"), @@ -51,5 +61,9 @@ name="leaflet_update_publisher_details", ), re_path(r"^$", LatestLeaflets.as_view(), name="leaflets"), - re_path(r"^moderate$", login_required(LeafletModeration.as_view()), name="moderate"), + re_path( + r"^moderate$", + login_required(LeafletModeration.as_view()), + name="moderate", + ), ] diff --git a/electionleaflets/apps/leaflets/views.py b/electionleaflets/apps/leaflets/views.py index fbd40c4b..d71a934f 100644 --- a/electionleaflets/apps/leaflets/views.py +++ b/electionleaflets/apps/leaflets/views.py @@ -1,30 +1,29 @@ -import json import datetime +import json import random from urllib.parse import urljoin -from django.core.files.storage import default_storage +from braces.views import LoginRequiredMixin, StaffuserRequiredMixin +from core.helpers import CacheControlMixin +from django.conf import settings +from django.contrib import messages +from django.core.signing import Signer from django.db import transaction +from django.http import HttpResponseRedirect from django.shortcuts import redirect -from django.http import HttpResponseRedirect, HttpResponseForbidden -from django.contrib import messages from django.urls import reverse -from formtools.wizard.views import NamedUrlSessionWizardView -from django.core.signing import Signer -from django.conf import settings -from django.views.generic import DetailView, ListView, UpdateView, RedirectView +from django.views.generic import DetailView, ListView, RedirectView, UpdateView from django.views.generic.detail import SingleObjectMixin -from braces.views import StaffuserRequiredMixin, LoginRequiredMixin +from formtools.wizard.views import NamedUrlSessionWizardView +from people.models import Person +from storages.backends.s3boto3 import S3Boto3Storage -from core.helpers import CacheControlMixin -from .models import Leaflet, LeafletImage from .forms import ( LeafletDetailsFrom, SingleLeafletImageForm, UpdatePublisherDetails, ) -from people.models import Person -from storages.backends.s3boto3 import S3Boto3Storage +from .models import Leaflet, LeafletImage class ImageView(CacheControlMixin, UpdateView): @@ -97,6 +96,7 @@ class LeafletView(CacheControlMixin, DetailView): template_name = "leaflets/leaflet.html" model = Leaflet + def should_show_party_form(wizard): party_form = wizard.get_form("party") postcode_dict = wizard.get_cleaned_data_for_step("postcode") @@ -104,10 +104,7 @@ def should_show_party_form(wizard): if not postcode_dict: return False postcode = postcode_dict.get("postcode") - ballot_data = party_form.get_ballot_data_from_ynr(postcode) - if not ballot_data: - return False - return True + return party_form.get_ballot_data_from_ynr(postcode) def should_show_person_form(wizard): @@ -118,14 +115,14 @@ def should_show_person_form(wizard): signer = Signer() selected_party = json.loads(signer.unsign(cleaned_data["party"])) return selected_party.get("has_candidates", False) + return None def should_show_date_form(wizard): cleaned_data = wizard.get_cleaned_data_for_step("postcode") or {} if not cleaned_data: return False - if cleaned_data.get("delivered") == "before": - return True + return cleaned_data.get("delivered") == "before" class LeafletUploadWizzard(NamedUrlSessionWizardView): @@ -150,14 +147,14 @@ def get_form_initial(self, step): if step in ["party", "people"]: postcode = self.get_cleaned_data_for_step("postcode") if not postcode: - return + return None ret = {"postcode": postcode.get("postcode")} try: date = self.get_cleaned_data_for_step("date")["date"] except (KeyError, TypeError): date = datetime.datetime.now() ret["for_date"] = date - + party_data = self.storage.get_step_data("party") if not party_data: return ret @@ -167,6 +164,7 @@ def get_form_initial(self, step): "party-party" ] return ret + return None def get_context_data(self, **kwargs): context = super(LeafletUploadWizzard, self).get_context_data(**kwargs) @@ -179,9 +177,6 @@ def get(self, *args, **kwargs): return HttpResponseRedirect("/") return super(LeafletUploadWizzard, self).get(*args, **kwargs) - - - @transaction.atomic def done(self, form_list, **kwargs): # Create a new leaflet @@ -222,38 +217,35 @@ def done(self, form_list, **kwargs): leaflet.ynr_party_id = party_data["party_id"] leaflet.ynr_party_name = party_data["party_name"] - if form_prefix == "people": - if ( - "people" in form.cleaned_data - and isinstance(form.cleaned_data["people"], list) - and form.cleaned_data["people"] != "" - ): - leaflet_people = {} - for person in form.cleaned_data["people"]: - - person_data = json.loads(signer.unsign(person)) - if not person_data: - continue - leaflet_people[ - person_data["person"]["id"] - ] = person_data - person, _ = Person.objects.get_or_create( - remote_id=person_data["person"]["id"], - defaults={ - "name": person_data["person"]["name"], - "source_url": urljoin( - settings.YNR_BASE_URL, - f"/person/{person_data['person']['id']}" - ), - "source_name": "YNR2017", - }, - ) - - leaflet.people = leaflet_people - leaflet.person_ids = list(leaflet_people.keys()) - leaflet.ballots = [ - c["ballot"] for ynr_id, c in leaflet_people.items() - ] + if ( + form_prefix == "people" + and "people" in form.cleaned_data + and isinstance(form.cleaned_data["people"], list) + and form.cleaned_data["people"] != "" + ): + leaflet_people = {} + for person in form.cleaned_data["people"]: + person_data = json.loads(signer.unsign(person)) + if not person_data: + continue + leaflet_people[person_data["person"]["id"]] = person_data + person, _ = Person.objects.get_or_create( + remote_id=person_data["person"]["id"], + defaults={ + "name": person_data["person"]["name"], + "source_url": urljoin( + settings.YNR_BASE_URL, + f"/person/{person_data['person']['id']}", + ), + "source_name": "YNR2017", + }, + ) + + leaflet.people = leaflet_people + leaflet.person_ids = list(leaflet_people.keys()) + leaflet.ballots = [ + c["ballot"] for ynr_id, c in leaflet_people.items() + ] leaflet.save() messages.success( @@ -276,7 +268,6 @@ def form_valid(self, form): signer = Signer() leaflet_people = {} for person in form.cleaned_data["people"]: - person_data = json.loads(signer.unsign(person)) if not person_data: continue @@ -287,7 +278,7 @@ def form_valid(self, form): "name": person_data["person"]["name"], "source_url": urljoin( settings.YNR_BASE_URL, - f"/person/{person_data['person']['id']}" + f"/person/{person_data['person']['id']}", ), "source_name": "YNR2017", }, @@ -311,7 +302,6 @@ class LeafletModeration(ListView): queryset = Leaflet.objects.filter(status="draft")[:10] template_name = "leaflets/moderation_queue.html" - def post(self, request): leaflet = Leaflet.objects.get(pk=self.request.POST.get("leaflet")) leaflet.status = "live" diff --git a/electionleaflets/apps/legacy/routers.py b/electionleaflets/apps/legacy/routers.py index 2f8c245d..6464a69a 100644 --- a/electionleaflets/apps/legacy/routers.py +++ b/electionleaflets/apps/legacy/routers.py @@ -35,6 +35,6 @@ def allow_migrate(self, db, model): """ if db == "auth_db": return model._meta.app_label == "legacy" - elif model._meta.app_label == "legacy": + if model._meta.app_label == "legacy": return False return None diff --git a/electionleaflets/apps/parties/views.py b/electionleaflets/apps/parties/views.py index 9eb32afc..68a399d5 100644 --- a/electionleaflets/apps/parties/views.py +++ b/electionleaflets/apps/parties/views.py @@ -1,28 +1,24 @@ -from datetime import datetime import re +from datetime import datetime -from django.http import Http404 -from django.views.generic import DetailView, ListView, TemplateView +from django.core.paginator import EmptyPage, PageNotAnInteger, Paginator from django.db.models import Count, Q -from django.core.paginator import Paginator, EmptyPage, PageNotAnInteger - -from uk_political_parties.models import Party +from django.http import Http404 +from django.views.generic import ListView, TemplateView from leaflets.models import Leaflet +from uk_political_parties.models import Party class PartyList(ListView): def get_queryset(self): - - queryset = Party.objects.annotate( - num_leaflets=Count("leaflet") - ).order_by("-num_leaflets", "party_name") - return queryset + return Party.objects.annotate(num_leaflets=Count("leaflet")).order_by( + "-num_leaflets", "party_name" + ) template_name = "parties/party_list.html" class PartyView(TemplateView): - def get_context_data(self, **kwargs): context = super(PartyView, self).get_context_data(**kwargs) id = re.sub(r"[^0-9]", "", self.kwargs["pk"]) @@ -39,11 +35,10 @@ def get_context_data(self, **kwargs): paginator = Paginator(qs, 60) page = self.request.GET.get("page") - if not page or page == 1: - if qs: - context["last_leaflet_days"] = ( - datetime.now() - qs[0].date_uploaded - ).days + if not page or page == 1 and qs: + context["last_leaflet_days"] = ( + datetime.now() - qs[0].date_uploaded + ).days try: context["party_leaflets"] = paginator.page(page) diff --git a/electionleaflets/apps/people/management/commands/people_import_ynmp_2015.py b/electionleaflets/apps/people/management/commands/people_import_ynmp_2015.py index 15d12758..80e4c251 100644 --- a/electionleaflets/apps/people/management/commands/people_import_ynmp_2015.py +++ b/electionleaflets/apps/people/management/commands/people_import_ynmp_2015.py @@ -1,15 +1,13 @@ import csv -import io import datetime +import io import requests - +from constituencies.models import Constituency from django.core.management.base import BaseCommand - from elections.models import Election -from constituencies.models import Constituency +from people.models import PartyMemberships, Person, PersonConstituencies from uk_political_parties.models import Party -from people.models import Person, PartyMemberships, PersonConstituencies SOURCE = "YNMP2015" diff --git a/electionleaflets/apps/people/management/commands/people_import_ynr_parl_2017.py b/electionleaflets/apps/people/management/commands/people_import_ynr_parl_2017.py index 11b7d2dd..931bc4cf 100644 --- a/electionleaflets/apps/people/management/commands/people_import_ynr_parl_2017.py +++ b/electionleaflets/apps/people/management/commands/people_import_ynr_parl_2017.py @@ -2,13 +2,11 @@ import datetime import requests - +from constituencies.models import Constituency from django.core.management.base import BaseCommand - from elections.models import Election -from constituencies.models import Constituency +from people.models import PartyMemberships, Person, PersonConstituencies from uk_political_parties.models import Party -from people.models import Person, PartyMemberships, PersonConstituencies SOURCE = "YNR2017" diff --git a/electionleaflets/apps/people/migrations/0001_initial.py b/electionleaflets/apps/people/migrations/0001_initial.py index 49468356..f73ec0c1 100644 --- a/electionleaflets/apps/people/migrations/0001_initial.py +++ b/electionleaflets/apps/people/migrations/0001_initial.py @@ -2,12 +2,11 @@ # Generated by Django 1.9.5 on 2016-04-03 14:56 -from django.db import migrations, models import django.db.models.deletion +from django.db import migrations, models class Migration(migrations.Migration): - initial = True dependencies = [ diff --git a/electionleaflets/apps/people/migrations/0002_import_ynr_people.py b/electionleaflets/apps/people/migrations/0002_import_ynr_people.py index f658676e..4f04be12 100644 --- a/electionleaflets/apps/people/migrations/0002_import_ynr_people.py +++ b/electionleaflets/apps/people/migrations/0002_import_ynr_people.py @@ -30,7 +30,6 @@ def import_people(apps, _): class Migration(migrations.Migration): - dependencies = [ ("leaflets", "0001_initial"), ("people", "0001_initial"), diff --git a/electionleaflets/apps/people/migrations/0003_remove_duplicates.py b/electionleaflets/apps/people/migrations/0003_remove_duplicates.py index 28ade828..205d015d 100644 --- a/electionleaflets/apps/people/migrations/0003_remove_duplicates.py +++ b/electionleaflets/apps/people/migrations/0003_remove_duplicates.py @@ -26,13 +26,12 @@ def remove_dupes(apps, schema_editor): collector.collect([person]) collected = collector.nested() assert len(collected) < 2 or not any( - [isinstance(x, Leaflet) for x in collected[1]] + isinstance(x, Leaflet) for x in collected[1] ) person.delete() class Migration(migrations.Migration): - dependencies = [ ("people", "0001_initial"), ] diff --git a/electionleaflets/apps/people/migrations/0004_unique_remote_id.py b/electionleaflets/apps/people/migrations/0004_unique_remote_id.py index 72f963dc..22ed3b4c 100644 --- a/electionleaflets/apps/people/migrations/0004_unique_remote_id.py +++ b/electionleaflets/apps/people/migrations/0004_unique_remote_id.py @@ -6,7 +6,6 @@ class Migration(migrations.Migration): - dependencies = [ ("people", "0003_remove_duplicates"), ] diff --git a/electionleaflets/apps/people/migrations/0005_merge_20210425_1928.py b/electionleaflets/apps/people/migrations/0005_merge_20210425_1928.py index f5b21cac..ce62cbf1 100644 --- a/electionleaflets/apps/people/migrations/0005_merge_20210425_1928.py +++ b/electionleaflets/apps/people/migrations/0005_merge_20210425_1928.py @@ -4,7 +4,6 @@ class Migration(migrations.Migration): - dependencies = [ ("people", "0002_import_ynr_people"), ("people", "0004_unique_remote_id"), diff --git a/electionleaflets/apps/people/models.py b/electionleaflets/apps/people/models.py index ae2e62e4..3881f4b7 100644 --- a/electionleaflets/apps/people/models.py +++ b/electionleaflets/apps/people/models.py @@ -1,8 +1,7 @@ -from django.db import models - from constituencies.models import Constituency -from uk_political_parties.models import Party +from django.db import models from elections.models import Election +from uk_political_parties.models import Party class Person(models.Model): @@ -24,6 +23,7 @@ def current_party(self): parties = self.partymemberships_set.filter(membership_end=None) if parties: return parties[0] + return None @property def current_election(self): diff --git a/electionleaflets/apps/people/tests/test_models.py b/electionleaflets/apps/people/tests/test_models.py index 2696903b..c1bd3754 100644 --- a/electionleaflets/apps/people/tests/test_models.py +++ b/electionleaflets/apps/people/tests/test_models.py @@ -1,57 +1,76 @@ import pytest -from people.models import Person, PartyMemberships, PersonConstituencies from constituencies.models import Constituency -from uk_political_parties.models import Party from elections.models import Election +from people.models import PartyMemberships, Person, PersonConstituencies +from uk_political_parties.models import Party + @pytest.fixture def person(): return Person.objects.create(name="John Doe") + @pytest.fixture def party(): return Party.objects.create(party_name="Test Party") + @pytest.fixture def constituency(): return Constituency.objects.create(name="Test Constituency") + @pytest.fixture def election(): - return Election.objects.create(name="Test Election", live_date="2023-01-01", dead_date="2023-01-01") + return Election.objects.create( + name="Test Election", live_date="2023-01-01", dead_date="2023-01-01" + ) @pytest.mark.django_db def test_create_party_membership(person, party): - membership = PartyMemberships.objects.create(person=person, party=party, membership_start="2023-01-01") + membership = PartyMemberships.objects.create( + person=person, party=party, membership_start="2023-01-01" + ) assert membership.person == person assert membership.party == party assert membership.membership_start == "2023-01-01" assert membership.membership_end is None + @pytest.mark.django_db def test_create_person_constituency(person, constituency, election): - person_constituency = PersonConstituencies.objects.create(person=person, constituency=constituency, election=election) + person_constituency = PersonConstituencies.objects.create( + person=person, constituency=constituency, election=election + ) assert person_constituency.person == person assert person_constituency.constituency == constituency assert person_constituency.election == election + @pytest.mark.django_db def test_person_current_party(person, party): - PartyMemberships.objects.create(person=person, party=party, membership_start="2023-01-01") + PartyMemberships.objects.create( + person=person, party=party, membership_start="2023-01-01" + ) assert person.current_party.party == party + @pytest.mark.django_db def test_person_current_election(person, election): person.elections.add(election) assert person.current_election == election + @pytest.mark.django_db def test_person_current_constituency(person, constituency, election): - PersonConstituencies.objects.create(person=person, constituency=constituency, election=election) + PersonConstituencies.objects.create( + person=person, constituency=constituency, election=election + ) person.elections.add(election) assert person.current_constituency == constituency + @pytest.mark.django_db def test_person_unicode(person): - assert str(person.name) == "John Doe" \ No newline at end of file + assert str(person.name) == "John Doe" diff --git a/electionleaflets/apps/people/views.py b/electionleaflets/apps/people/views.py index 818be64b..a42f1a92 100644 --- a/electionleaflets/apps/people/views.py +++ b/electionleaflets/apps/people/views.py @@ -1,11 +1,9 @@ from datetime import datetime +from django.core.paginator import EmptyPage, PageNotAnInteger, Paginator from django.views.generic import DetailView -from django.core.paginator import Paginator, EmptyPage, PageNotAnInteger - - -from people.models import Person from leaflets.models import Leaflet +from people.models import Person class PersonView(DetailView): @@ -21,11 +19,10 @@ def get_context_data(self, **kwargs): paginator = Paginator(qs, 60) page = self.request.GET.get("page") - if not page or page == 1: - if qs: - context["last_leaflet_days"] = ( - datetime.now() - qs[0].date_uploaded - ).days + if not page or page == 1 and qs: + context["last_leaflet_days"] = ( + datetime.now() - qs[0].date_uploaded + ).days try: context["person_leaflets"] = paginator.page(page) diff --git a/electionleaflets/lambda_awsgi.py b/electionleaflets/lambda_awsgi.py index 56cfdddb..286ff75a 100644 --- a/electionleaflets/lambda_awsgi.py +++ b/electionleaflets/lambda_awsgi.py @@ -9,7 +9,9 @@ path.append(SITE_ROOT) -os.environ.setdefault("DJANGO_SETTINGS_MODULE", "electionleaflets.settings.base_lambda") +os.environ.setdefault( + "DJANGO_SETTINGS_MODULE", "electionleaflets.settings.base_lambda" +) application = get_wsgi_application() @@ -27,4 +29,4 @@ def handler(event, context): "image/jpg", "font/woff2", }, - ) \ No newline at end of file + ) diff --git a/electionleaflets/settings/base.py b/electionleaflets/settings/base.py index c9d5eb00..d5bc0dbb 100644 --- a/electionleaflets/settings/base.py +++ b/electionleaflets/settings/base.py @@ -1,9 +1,12 @@ +import os import sys from os import environ -import os # PATH vars -from os.path import join, abspath, dirname +from os.path import abspath, dirname, join + +import dc_design_system + def here(x): return join(abspath(dirname(__file__)), x) @@ -23,7 +26,7 @@ def root(x): template_DEBUG = DEBUG TEMPLATE_DEBUG = DEBUG -DEFAULT_AUTO_FIELD = 'django.db.models.AutoField' +DEFAULT_AUTO_FIELD = "django.db.models.AutoField" # DATABASES define in environment specific settings file DATABASES = { @@ -39,7 +42,9 @@ def root(x): ALLOWED_HOSTS = ["*"] -MEDIA_ROOT = root("media",) +MEDIA_ROOT = root( + "media", +) MEDIA_URL = "/media/" STATIC_ROOT = root("static") STATIC_URL = "/static/" @@ -67,7 +72,9 @@ def root(x): "scss/vendor/filepond-plugin-image-preview.css", ], "output_filename": "scss/styles.css", - "extra_context": {"media": "screen,projection",}, + "extra_context": { + "media": "screen,projection", + }, }, }, "JAVASCRIPT": { @@ -89,7 +96,6 @@ def root(x): PIPELINE["CSS_COMPRESSOR"] = "pipeline.compressors.NoopCompressor" PIPELINE["JS_COMPRESSOR"] = "pipeline.compressors.NoopCompressor" -import dc_design_system PIPELINE["SASS_ARGUMENTS"] = ( " -I " + dc_design_system.DC_SYSTEM_PATH + "/system" @@ -200,7 +206,9 @@ def setup_sentry(environment=None): TEMPLATES = [ { "BACKEND": "django.template.backends.django.DjangoTemplates", - "DIRS": [root("templates"),], + "DIRS": [ + root("templates"), + ], "APP_DIRS": True, "OPTIONS": { "context_processors": [ @@ -244,7 +252,7 @@ def setup_sentry(environment=None): YNR_API_KEY = None YNR_BASE_URL = "https://candidates.democracyclub.org.uk" -if not "testing" in environ.get("DJANGO_SETTINGS_MODULE", ""): +if "testing" not in environ.get("DJANGO_SETTINGS_MODULE", ""): # .local.py overrides all the common settings. try: from .local import * # noqa: F401,F403 diff --git a/electionleaflets/settings/base_lambda.py b/electionleaflets/settings/base_lambda.py index 297bda9e..40ec0e6f 100644 --- a/electionleaflets/settings/base_lambda.py +++ b/electionleaflets/settings/base_lambda.py @@ -1,9 +1,10 @@ import os from django.urls import set_urlconf + from .base import * # noqa: F401,F403 -ALLOWED_HOSTS = [os.environ.get('APP_DOMAIN')] +ALLOWED_HOSTS = [os.environ.get("APP_DOMAIN")] DATABASES = { "default": { @@ -30,11 +31,13 @@ STATIC_URL = WHITENOISE_STATIC_PREFIX STATICFILES_STORAGE = "electionleaflets.storages.StaticStorage" -STATICFILES_DIRS = (root("assets"),) -STATIC_ROOT = root("static") -MEDIA_ROOT = root("media",) +STATICFILES_DIRS = (root("assets"),) # noqa: F405 +STATIC_ROOT = root("static") # noqa: F405 +MEDIA_ROOT = root( # noqa: F405 + "media", +) MEDIA_URL = "/media/" -set_urlconf(ROOT_URLCONF) +set_urlconf(ROOT_URLCONF) # noqa: F405 AWS_DEFAULT_ACL = "public-read" @@ -63,4 +66,4 @@ CSRF_TRUSTED_ORIGINS = ["https://electionleaflets.org"] USE_X_FORWARDED_HOST = True -setup_sentry() +setup_sentry() # noqa: F405 diff --git a/electionleaflets/settings/testing.py b/electionleaflets/settings/testing.py index bb477ca4..36055460 100644 --- a/electionleaflets/settings/testing.py +++ b/electionleaflets/settings/testing.py @@ -2,6 +2,7 @@ from tempfile import mkdtemp from .base import * # noqa: F403 + os.environ["DJANGO_ALLOW_ASYNC_UNSAFE"] = "true" DEFAULT_FILE_STORAGE = "electionleaflets.storages.TempUploadLocalMediaStorage" diff --git a/electionleaflets/storages.py b/electionleaflets/storages.py index 81dca31c..255a38b6 100644 --- a/electionleaflets/storages.py +++ b/electionleaflets/storages.py @@ -1,6 +1,6 @@ import abc from pathlib import Path -from urllib.parse import urlsplit, unquote, urlunsplit +from urllib.parse import unquote, urlsplit, urlunsplit from django.contrib.staticfiles.storage import ManifestStaticFilesStorage from django.core.files.storage import FileSystemStorage, default_storage @@ -53,8 +53,7 @@ class TempUploadBaseMixin(abc.ABC): """ @abc.abstractmethod - def save_from_temp_upload(self, source_path, target_file_path): - ... + def save_from_temp_upload(self, source_path, target_file_path): ... class TempUploadS3MediaStorage(TempUploadBaseMixin, S3Boto3Storage): @@ -70,10 +69,13 @@ def copy_file(self, source_path, target_file_path): moved_file.copy(copy_source) return moved_file + class TempUploadLocalMediaStorage(TempUploadBaseMixin, FileSystemStorage): def save_from_temp_upload(self, source_path, target_file_path: Path): target_file_path = Path(default_storage.path(target_file_path)) target_file_path.parent.mkdir(parents=True, exist_ok=True) - with open(default_storage.path(source_path), "rb") as source_file: - with target_file_path.open("wb") as dest_file: - dest_file.write(source_file.read()) + with ( + open(default_storage.path(source_path), "rb") as source_file, + target_file_path.open("wb") as dest_file, + ): + dest_file.write(source_file.read()) diff --git a/electionleaflets/urls.py b/electionleaflets/urls.py index 7ace6280..e3f4496c 100644 --- a/electionleaflets/urls.py +++ b/electionleaflets/urls.py @@ -1,17 +1,21 @@ import debug_toolbar from django.conf import settings -from django.contrib import admin from django.conf.urls.static import static -from django.urls import path, re_path, include -from django.views.generic import TemplateView, RedirectView +from django.contrib import admin +from django.urls import include, path, re_path +from django.views.generic import RedirectView, TemplateView from electionleaflets.apps.api.feeds import ConstituencyFeed, LatestLeafletsFeed -from electionleaflets.apps.core.views import HomeView, MaintenanceView, ReportThanksView, ReportView +from electionleaflets.apps.core.views import ( + HomeView, + MaintenanceView, + ReportThanksView, + ReportView, +) admin.autodiscover() - MAINTENANCE_MODE = getattr(settings, "MAINTENANCE_MODE", False) if MAINTENANCE_MODE: urlpatterns = [ @@ -26,9 +30,7 @@ re_path(r"^person/", include("people.urls")), re_path(r"^api/", include("api.urls")), # Feeds - re_path( - r"^feeds/latest/$", LatestLeafletsFeed(), name="latest_feed" - ), + re_path(r"^feeds/latest/$", LatestLeafletsFeed(), name="latest_feed"), re_path( r"^feeds/constituency/(?P[\w_\-\.]+)/$", ConstituencyFeed(), @@ -60,16 +62,18 @@ ), # Administration URLS path("admin/", admin.site.urls), - path('__debug__/', include(debug_toolbar.urls)), + path("__debug__/", include(debug_toolbar.urls)), ] # Old redirects + class HomePageRedirectView(RedirectView): """ Just redirect to the home page. """ + permanent = True query_string = True pattern_name = "home" @@ -77,9 +81,7 @@ class HomePageRedirectView(RedirectView): redirect_urls = ( re_path(r"^analysis", HomePageRedirectView.as_view(), name="analysis"), - re_path( - r"^start/$", HomePageRedirectView.as_view(), name="analysis_start" - ), + re_path(r"^start/$", HomePageRedirectView.as_view(), name="analysis_start"), re_path( r"^tag_candidates/$", HomePageRedirectView.as_view(), diff --git a/electionleaflets/wsgi.py b/electionleaflets/wsgi.py index ad92db6c..c389ff28 100644 --- a/electionleaflets/wsgi.py +++ b/electionleaflets/wsgi.py @@ -8,7 +8,9 @@ path.append(SITE_ROOT) -os.environ.setdefault("DJANGO_SETTINGS_MODULE", "electionleaflets.settings.base_lambda") +os.environ.setdefault( + "DJANGO_SETTINGS_MODULE", "electionleaflets.settings.base_lambda" +) -application = get_wsgi_application() \ No newline at end of file +application = get_wsgi_application() diff --git a/pyproject.toml b/pyproject.toml index f5a516fe..52de6319 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -12,3 +12,14 @@ DJANGO_SETTINGS_MODULE = "electionleaflets.settings.testing" python_files = ["test_*.py", "*_test.py"] addopts = "--reuse-db --tb=short -p no:warnings" django_debug_mode = true + +[tool.ruff] +line-length = 80 +lint.ignore = ["E501"] +lint.extend-select = [ + "I", + "C4", + "SIM", + "Q003", + "RET", +] diff --git a/scripts/format.sh b/scripts/format.sh new file mode 100755 index 00000000..0f425227 --- /dev/null +++ b/scripts/format.sh @@ -0,0 +1,4 @@ +#!/bin/bash +set -euxo pipefail + +pipenv run ruff format . --check diff --git a/scripts/lint.sh b/scripts/lint.sh new file mode 100755 index 00000000..a7e4d875 --- /dev/null +++ b/scripts/lint.sh @@ -0,0 +1,4 @@ +#!/bin/bash +set -euxo pipefail + +pipenv run ruff check . diff --git a/thumbs/attach_lambda_triggers.py b/thumbs/attach_lambda_triggers.py index c9be3632..9026cbba 100644 --- a/thumbs/attach_lambda_triggers.py +++ b/thumbs/attach_lambda_triggers.py @@ -1,7 +1,7 @@ import os import boto3 -from botocore.exceptions import BotoCoreError +from botocore.exceptions import ClientError IMAGES_BUCKET_NAME = os.environ.get("LEAFLET_IMAGES_BUCKET_NAME") IMAGES_URL = f"images.{os.environ.get('PUBLIC_FQDN')}" @@ -17,12 +17,13 @@ def get_thumbs_function(lambda_client): f"ElectionLeafletsThumbs-{ENVIRONMENT}" ): return function + return None def policy_exists(arn): try: policy = lambda_client.get_policy(FunctionName=function_arn) - except: + except ClientError: return False return "s3_thumbs" in policy["Policy"] @@ -48,7 +49,10 @@ def policy_exists(arn): "Filter": { "Key": { "FilterRules": [ - {"Name": "prefix", "Value": "leaflets/",}, + { + "Name": "prefix", + "Value": "leaflets/", + }, {"Name": "Suffix", "Value": ""}, ] } @@ -67,6 +71,7 @@ def get_dist(cf_client): for dist in cf_client.list_distributions()["DistributionList"]["Items"]: if IMAGES_URL in dist["Aliases"]["Items"]: return dist + return None dist = get_dist(cf_client) diff --git a/thumbs/handler.py b/thumbs/handler.py index 23610db2..3487ed66 100644 --- a/thumbs/handler.py +++ b/thumbs/handler.py @@ -1,31 +1,30 @@ import base64 -import os -from io import BytesIO -from os.path import basename, dirname, splitext, sep -from os import makedirs import re import sys +from io import BytesIO +from os import makedirs +from os.path import dirname, sep sys.path.append("") from typing import Tuple from urllib.parse import unquote import boto3 - import django from django.conf import settings -settings.configure(THUMBNAIL_KVSTORE="thumbs.PassthruKVStore",) -BUCKET_NAME = open("LEAFLET_IMAGES_BUCKET_NAME").read().strip() +settings.configure( + THUMBNAIL_KVSTORE="thumbs.PassthruKVStore", +) +with open("LEAFLET_IMAGES_BUCKET_NAME") as f: + BUCKET_NAME = f.read().strip() django.setup() -from PIL import Image, ImageOps -from sorl.thumbnail import get_thumbnail -from sorl.thumbnail import conf as sorl_conf -from sorl.thumbnail.base import ThumbnailBackend -from sorl.thumbnail.engines import pil_engine -from sorl.thumbnail.parsers import parse_crop, parse_geometry - +# These need to be imported after Django has bootstrapped +from PIL import Image # noqa: E402 +from sorl.thumbnail.base import ThumbnailBackend # noqa: E402 +from sorl.thumbnail.engines import pil_engine # noqa: E402 +from sorl.thumbnail.parsers import parse_geometry # noqa: E402 client = boto3.client("s3") engine = pil_engine.Engine() @@ -34,8 +33,9 @@ def handle(event, context): if "s3" in event["Records"][0]: return handle_s3(event, context) - elif "cf" in event["Records"][0]: + if "cf" in event["Records"][0]: return handle_cf(event, context) + return None def handle_cf(event, context):