diff --git a/.github/workflows/linter.yml b/.github/workflows/linter.yml index b7c108dc2f..cbf322bd10 100644 --- a/.github/workflows/linter.yml +++ b/.github/workflows/linter.yml @@ -7,27 +7,14 @@ on: - staging merge_group: -permissions: { } - jobs: lint: - name: Lint Code Base runs-on: ubuntu-latest - steps: - - name: Checkout Code - uses: actions/checkout@v4 - - - name: Ruff check - uses: chartboost/ruff-action@491342200cdd1cf4d5132a30ddc546b3b5bc531b + - uses: actions/checkout@v4 with: - version: 0.6.7 - args: "check" - changed-files: "true" - - - name: Ruff format - uses: chartboost/ruff-action@491342200cdd1cf4d5132a30ddc546b3b5bc531b + fetch-depth: 0 + - uses: actions/setup-python@v3 + - uses: pre-commit/action@v3.0.1 with: - version: 0.6.7 - args: "format" - changed-files: "true" + extra_args: --color=always --from-ref ${{ github.event.pull_request.base.sha }} --to-ref ${{ github.event.pull_request.head.sha }} diff --git a/Pipfile b/Pipfile index c469da3047..28d27192b8 100644 --- a/Pipfile +++ b/Pipfile @@ -44,7 +44,6 @@ sentry-sdk = "==2.14.0" whitenoise = "==6.7.0" [dev-packages] -black = "==24.8.0" boto3-stubs = { extras = ["s3", "boto3"], version = "==1.35.25" } coverage = "==7.6.1" debugpy = "==1.8.5" @@ -53,10 +52,8 @@ django-extensions = "==3.2.3" django-silk = "==5.2.0" djangorestframework-stubs = "==3.15.1" factory-boy = "==3.3.1" -flake8 = "==7.1.1" freezegun = "==1.5.1" ipython = "==8.27.0" -isort = "==5.13.2" mypy = "==1.11.2" pre-commit = "==3.8.0" requests-mock = "==1.12.1" diff --git a/Pipfile.lock b/Pipfile.lock index be658a3a2b..b43bb3e4f6 100644 --- a/Pipfile.lock +++ b/Pipfile.lock @@ -1,7 +1,7 @@ { "_meta": { "hash": { - "sha256": "41ec4e61d7dcec07332f048933e1c9699217a4b73422a8630121eedd2f6cffa4" + "sha256": "f249cd5eb0e4b4e2f285fe4b6d0a9b3a125e26ab0521692f3ad0ab5e332fc450" }, "pipfile-spec": 6, "requires": { @@ -470,7 +470,6 @@ "sha256:f98bf604c82c416bc829e490c700ca1553eafdf2912a91e23a79d97d9801372a", "sha256:fba1007b3ef89946dbbb515aeeb41e30203b004f0b4b00e5e16078b518563289" ], - "index": "pypi", "markers": "python_version >= '3.7'", "version": "==43.0.1" }, @@ -1616,11 +1615,11 @@ }, "tzdata": { "hashes": [ - "sha256:2674120f8d891909751c38abcdfd386ac0a5a1127954fbc332af6b5ceae07efd", - "sha256:9068bc196136463f5245e51efda838afa15aaeca9903f49050dfa2679db4d252" + "sha256:7d85cc416e9382e69095b7bdf4afd9e3880418a2413feec7069d533d6b4e31cc", + "sha256:a48093786cdcde33cad18c2555e8532f34422074448fbc874186f0abd79565cd" ], "markers": "python_version >= '2'", - "version": "==2024.1" + "version": "==2024.2" }, "unicodecsv": { "hashes": [ @@ -1670,101 +1669,101 @@ }, "yarl": { "hashes": [ - "sha256:01a8697ec24f17c349c4f655763c4db70eebc56a5f82995e5e26e837c6eb0e49", - "sha256:02da8759b47d964f9173c8675710720b468aa1c1693be0c9c64abb9d8d9a4867", - "sha256:04293941646647b3bfb1719d1d11ff1028e9c30199509a844da3c0f5919dc520", - "sha256:067b961853c8e62725ff2893226fef3d0da060656a9827f3f520fb1d19b2b68a", - "sha256:077da604852be488c9a05a524068cdae1e972b7dc02438161c32420fb4ec5e14", - "sha256:09696438cb43ea6f9492ef237761b043f9179f455f405279e609f2bc9100212a", - "sha256:0b8486f322d8f6a38539136a22c55f94d269addb24db5cb6f61adc61eabc9d93", - "sha256:0ea9682124fc062e3d931c6911934a678cb28453f957ddccf51f568c2f2b5e05", - "sha256:0f351fa31234699d6084ff98283cb1e852270fe9e250a3b3bf7804eb493bd937", - "sha256:14438dfc5015661f75f85bc5adad0743678eefee266ff0c9a8e32969d5d69f74", - "sha256:15061ce6584ece023457fb8b7a7a69ec40bf7114d781a8c4f5dcd68e28b5c53b", - "sha256:15439f3c5c72686b6c3ff235279630d08936ace67d0fe5c8d5bbc3ef06f5a420", - "sha256:17b5a386d0d36fb828e2fb3ef08c8829c1ebf977eef88e5367d1c8c94b454639", - "sha256:18ac56c9dd70941ecad42b5a906820824ca72ff84ad6fa18db33c2537ae2e089", - "sha256:1bb2d9e212fb7449b8fb73bc461b51eaa17cc8430b4a87d87be7b25052d92f53", - "sha256:1e969fa4c1e0b1a391f3fcbcb9ec31e84440253325b534519be0d28f4b6b533e", - "sha256:1fa2e7a406fbd45b61b4433e3aa254a2c3e14c4b3186f6e952d08a730807fa0c", - "sha256:2164cd9725092761fed26f299e3f276bb4b537ca58e6ff6b252eae9631b5c96e", - "sha256:21a7c12321436b066c11ec19c7e3cb9aec18884fe0d5b25d03d756a9e654edfe", - "sha256:238a21849dd7554cb4d25a14ffbfa0ef380bb7ba201f45b144a14454a72ffa5a", - "sha256:250e888fa62d73e721f3041e3a9abf427788a1934b426b45e1b92f62c1f68366", - "sha256:25861303e0be76b60fddc1250ec5986c42f0a5c0c50ff57cc30b1be199c00e63", - "sha256:267b24f891e74eccbdff42241c5fb4f974de2d6271dcc7d7e0c9ae1079a560d9", - "sha256:27fcb271a41b746bd0e2a92182df507e1c204759f460ff784ca614e12dd85145", - "sha256:2909fa3a7d249ef64eeb2faa04b7957e34fefb6ec9966506312349ed8a7e77bf", - "sha256:3257978c870728a52dcce8c2902bf01f6c53b65094b457bf87b2644ee6238ddc", - "sha256:327c724b01b8641a1bf1ab3b232fb638706e50f76c0b5bf16051ab65c868fac5", - "sha256:3de5292f9f0ee285e6bd168b2a77b2a00d74cbcfa420ed078456d3023d2f6dff", - "sha256:3fce4da3703ee6048ad4138fe74619c50874afe98b1ad87b2698ef95bf92c96d", - "sha256:3ff6b1617aa39279fe18a76c8d165469c48b159931d9b48239065767ee455b2b", - "sha256:400cd42185f92de559d29eeb529e71d80dfbd2f45c36844914a4a34297ca6f00", - "sha256:4179522dc0305c3fc9782549175c8e8849252fefeb077c92a73889ccbcd508ad", - "sha256:4307d9a3417eea87715c9736d050c83e8c1904e9b7aada6ce61b46361b733d92", - "sha256:476e20c433b356e16e9a141449f25161e6b69984fb4cdbd7cd4bd54c17844998", - "sha256:489fa8bde4f1244ad6c5f6d11bb33e09cf0d1d0367edb197619c3e3fc06f3d91", - "sha256:48a28bed68ab8fb7e380775f0029a079f08a17799cb3387a65d14ace16c12e2b", - "sha256:48dfd117ab93f0129084577a07287376cc69c08138694396f305636e229caa1a", - "sha256:4973eac1e2ff63cf187073cd4e1f1148dcd119314ab79b88e1b3fad74a18c9d5", - "sha256:498442e3af2a860a663baa14fbf23fb04b0dd758039c0e7c8f91cb9279799bff", - "sha256:501c503eed2bb306638ccb60c174f856cc3246c861829ff40eaa80e2f0330367", - "sha256:504cf0d4c5e4579a51261d6091267f9fd997ef58558c4ffa7a3e1460bd2336fa", - "sha256:61a5f2c14d0a1adfdd82258f756b23a550c13ba4c86c84106be4c111a3a4e413", - "sha256:637c7ddb585a62d4469f843dac221f23eec3cbad31693b23abbc2c366ad41ff4", - "sha256:66b63c504d2ca43bf7221a1f72fbe981ff56ecb39004c70a94485d13e37ebf45", - "sha256:67459cf8cf31da0e2cbdb4b040507e535d25cfbb1604ca76396a3a66b8ba37a6", - "sha256:688654f8507464745ab563b041d1fb7dab5d9912ca6b06e61d1c4708366832f5", - "sha256:6907daa4b9d7a688063ed098c472f96e8181733c525e03e866fb5db480a424df", - "sha256:69721b8effdb588cb055cc22f7c5105ca6fdaa5aeb3ea09021d517882c4a904c", - "sha256:6d23754b9939cbab02c63434776df1170e43b09c6a517585c7ce2b3d449b7318", - "sha256:7175a87ab8f7fbde37160a15e58e138ba3b2b0e05492d7351314a250d61b1591", - "sha256:72bf26f66456baa0584eff63e44545c9f0eaed9b73cb6601b647c91f14c11f38", - "sha256:74db2ef03b442276d25951749a803ddb6e270d02dda1d1c556f6ae595a0d76a8", - "sha256:750f656832d7d3cb0c76be137ee79405cc17e792f31e0a01eee390e383b2936e", - "sha256:75e0ae31fb5ccab6eda09ba1494e87eb226dcbd2372dae96b87800e1dcc98804", - "sha256:768ecc550096b028754ea28bf90fde071c379c62c43afa574edc6f33ee5daaec", - "sha256:7d51324a04fc4b0e097ff8a153e9276c2593106a811704025bbc1d6916f45ca6", - "sha256:7e975a2211952a8a083d1b9d9ba26472981ae338e720b419eb50535de3c02870", - "sha256:8215f6f21394d1f46e222abeb06316e77ef328d628f593502d8fc2a9117bde83", - "sha256:8258c86f47e080a258993eed877d579c71da7bda26af86ce6c2d2d072c11320d", - "sha256:8418c053aeb236b20b0ab8fa6bacfc2feaaf7d4683dd96528610989c99723d5f", - "sha256:87f020d010ba80a247c4abc335fc13421037800ca20b42af5ae40e5fd75e7909", - "sha256:884eab2ce97cbaf89f264372eae58388862c33c4f551c15680dd80f53c89a269", - "sha256:8a336eaa7ee7e87cdece3cedb395c9657d227bfceb6781295cf56abcd3386a26", - "sha256:8aef1b64da41d18026632d99a06b3fefe1d08e85dd81d849fa7c96301ed22f1b", - "sha256:8aef97ba1dd2138112890ef848e17d8526fe80b21f743b4ee65947ea184f07a2", - "sha256:8ed653638ef669e0efc6fe2acb792275cb419bf9cb5c5049399f3556995f23c7", - "sha256:9361628f28f48dcf8b2f528420d4d68102f593f9c2e592bfc842f5fb337e44fd", - "sha256:946eedc12895873891aaceb39bceb484b4977f70373e0122da483f6c38faaa68", - "sha256:94d0caaa912bfcdc702a4204cd5e2bb01eb917fc4f5ea2315aa23962549561b0", - "sha256:964a428132227edff96d6f3cf261573cb0f1a60c9a764ce28cda9525f18f7786", - "sha256:999bfee0a5b7385a0af5ffb606393509cfde70ecca4f01c36985be6d33e336da", - "sha256:a08ea567c16f140af8ddc7cb58e27e9138a1386e3e6e53982abaa6f2377b38cc", - "sha256:a28b70c9e2213de425d9cba5ab2e7f7a1c8ca23a99c4b5159bf77b9c31251447", - "sha256:a34e1e30f1774fa35d37202bbeae62423e9a79d78d0874e5556a593479fdf239", - "sha256:a4264515f9117be204935cd230fb2a052dd3792789cc94c101c535d349b3dab0", - "sha256:a7915ea49b0c113641dc4d9338efa9bd66b6a9a485ffe75b9907e8573ca94b84", - "sha256:aac44097d838dda26526cffb63bdd8737a2dbdf5f2c68efb72ad83aec6673c7e", - "sha256:b91044952da03b6f95fdba398d7993dd983b64d3c31c358a4c89e3c19b6f7aef", - "sha256:ba444bdd4caa2a94456ef67a2f383710928820dd0117aae6650a4d17029fa25e", - "sha256:c2dc4250fe94d8cd864d66018f8344d4af50e3758e9d725e94fecfa27588ff82", - "sha256:c35f493b867912f6fda721a59cc7c4766d382040bdf1ddaeeaa7fa4d072f4675", - "sha256:c92261eb2ad367629dc437536463dc934030c9e7caca861cc51990fe6c565f26", - "sha256:ce928c9c6409c79e10f39604a7e214b3cb69552952fbda8d836c052832e6a979", - "sha256:d95b52fbef190ca87d8c42f49e314eace4fc52070f3dfa5f87a6594b0c1c6e46", - "sha256:dae7bd0daeb33aa3e79e72877d3d51052e8b19c9025ecf0374f542ea8ec120e4", - "sha256:e286580b6511aac7c3268a78cdb861ec739d3e5a2a53b4809faef6b49778eaff", - "sha256:e4b53f73077e839b3f89c992223f15b1d2ab314bdbdf502afdc7bb18e95eae27", - "sha256:e8f63904df26d1a66aabc141bfd258bf738b9bc7bc6bdef22713b4f5ef789a4c", - "sha256:f3a6d90cab0bdf07df8f176eae3a07127daafcf7457b997b2bf46776da2c7eb7", - "sha256:f41fa79114a1d2eddb5eea7b912d6160508f57440bd302ce96eaa384914cd265", - "sha256:f46f81501160c28d0c0b7333b4f7be8983dbbc161983b6fb814024d1b4952f79", - "sha256:f61db3b7e870914dbd9434b560075e0366771eecbe6d2b5561f5bc7485f39efd" - ], - "markers": "python_version >= '3.8'", - "version": "==1.11.1" + "sha256:0103c52f8dfe5d573c856322149ddcd6d28f51b4d4a3ee5c4b3c1b0a05c3d034", + "sha256:01549468858b87d36f967c97d02e6e54106f444aeb947ed76f8f71f85ed07cec", + "sha256:0274b1b7a9c9c32b7bf250583e673ff99fb9fccb389215841e2652d9982de740", + "sha256:0ac33d22b2604b020569a82d5f8a03ba637ba42cc1adf31f616af70baf81710b", + "sha256:0d0a5e87bc48d76dfcfc16295201e9812d5f33d55b4a0b7cad1025b92bf8b91b", + "sha256:10b690cd78cbaca2f96a7462f303fdd2b596d3978b49892e4b05a7567c591572", + "sha256:126309c0f52a2219b3d1048aca00766429a1346596b186d51d9fa5d2070b7b13", + "sha256:15871130439ad10abb25a4631120d60391aa762b85fcab971411e556247210a0", + "sha256:17d4dc4ff47893a06737b8788ed2ba2f5ac4e8bb40281c8603920f7d011d5bdd", + "sha256:18c2a7757561f05439c243f517dbbb174cadfae3a72dee4ae7c693f5b336570f", + "sha256:1d4017e78fb22bc797c089b746230ad78ecd3cdb215bc0bd61cb72b5867da57e", + "sha256:1f50a37aeeb5179d293465e522fd686080928c4d89e0ff215e1f963405ec4def", + "sha256:20d817c0893191b2ab0ba30b45b77761e8dfec30a029b7c7063055ca71157f84", + "sha256:22839d1d1eab9e4b427828a88a22beb86f67c14d8ff81175505f1cc8493f3500", + "sha256:22dda2799c8d39041d731e02bf7690f0ef34f1691d9ac9dfcb98dd1e94c8b058", + "sha256:2376d8cf506dffd0e5f2391025ae8675b09711016656590cb03b55894161fcfa", + "sha256:24197ba3114cc85ddd4091e19b2ddc62650f2e4a899e51b074dfd52d56cf8c72", + "sha256:24416bb5e221e29ddf8aac5b97e94e635ca2c5be44a1617ad6fe32556df44294", + "sha256:2631c9d7386bd2d4ce24ecc6ebf9ae90b3efd713d588d90504eaa77fec4dba01", + "sha256:28389a68981676bf74e2e199fe42f35d1aa27a9c98e3a03e6f58d2d3d054afe1", + "sha256:2aee7594d2c2221c717a8e394bbed4740029df4c0211ceb0f04815686e99c795", + "sha256:2e430ac432f969ef21770645743611c1618362309e3ad7cab45acd1ad1a540ff", + "sha256:2e912b282466444023610e4498e3795c10e7cfd641744524876239fcf01d538d", + "sha256:30ffc046ebddccb3c4cac72c1a3e1bc343492336f3ca86d24672e90ccc5e788a", + "sha256:319c206e83e46ec2421b25b300c8482b6fe8a018baca246be308c736d9dab267", + "sha256:326b8a079a9afcac0575971e56dabdf7abb2ea89a893e6949b77adfeb058b50e", + "sha256:36ee0115b9edca904153a66bb74a9ff1ce38caff015de94eadfb9ba8e6ecd317", + "sha256:3e26e64f42bce5ddf9002092b2c37b13071c2e6413d5c05f9fa9de58ed2f7749", + "sha256:4ea99e64b2ad2635e0f0597b63f5ea6c374791ff2fa81cdd4bad8ed9f047f56f", + "sha256:501a1576716032cc6d48c7c47bcdc42d682273415a8f2908e7e72cb4625801f3", + "sha256:54c8cee662b5f8c30ad7eedfc26123f845f007798e4ff1001d9528fe959fd23c", + "sha256:595bbcdbfc4a9c6989d7489dca8510cba053ff46b16c84ffd95ac8e90711d419", + "sha256:5b860055199aec8d6fe4dcee3c5196ce506ca198a50aab0059ffd26e8e815828", + "sha256:5c667b383529520b8dd6bd496fc318678320cb2a6062fdfe6d3618da6b8790f6", + "sha256:5fb475a4cdde582c9528bb412b98f899680492daaba318231e96f1a0a1bb0d53", + "sha256:607d12f0901f6419a8adceb139847c42c83864b85371f58270e42753f9780fa6", + "sha256:64c5b0f2b937fe40d0967516eee5504b23cb247b8b7ffeba7213a467d9646fdc", + "sha256:664380c7ed524a280b6a2d5d9126389c3e96cd6e88986cdb42ca72baa27421d6", + "sha256:6af871f70cfd5b528bd322c65793b5fd5659858cdfaa35fbe563fb99b667ed1f", + "sha256:6c89894cc6f6ddd993813e79244b36b215c14f65f9e4f1660b1f2ba9e5594b95", + "sha256:6dee0496d5f1a8f57f0f28a16f81a2033fc057a2cf9cd710742d11828f8c80e2", + "sha256:6e9a9f50892153bad5046c2a6df153224aa6f0573a5a8ab44fc54a1e886f6e21", + "sha256:712ba8722c0699daf186de089ddc4677651eb9875ed7447b2ad50697522cbdd9", + "sha256:717f185086bb9d817d4537dd18d5df5d657598cd00e6fc22e4d54d84de266c1d", + "sha256:71978ba778948760cff528235c951ea0ef7a4f9c84ac5a49975f8540f76c3f73", + "sha256:71af3766bb46738d12cc288d9b8de7ef6f79c31fd62757e2b8a505fe3680b27f", + "sha256:73a183042ae0918c82ce2df38c3db2409b0eeae88e3afdfc80fb67471a95b33b", + "sha256:7564525a4673fde53dee7d4c307a961c0951918f0b8c7f09b2c9e02067cf6504", + "sha256:76a59d1b63de859398bc7764c860a769499511463c1232155061fe0147f13e01", + "sha256:7e9905fc2dc1319e4c39837b906a024cf71b1261cc66b0cd89678f779c0c61f5", + "sha256:8112f640a4f7e7bf59f7cabf0d47a29b8977528c521d73a64d5cc9e99e48a174", + "sha256:835010cc17d0020e7931d39e487d72c8e01c98e669b6896a8b8c9aa8ca69a949", + "sha256:838dde2cb570cfbb4cab8a876a0974e8b90973ea40b3ac27a79b8a74c8a2db15", + "sha256:8d31dd0245d88cf7239e96e8f2a99f815b06e458a5854150f8e6f0e61618d41b", + "sha256:96b34830bd6825ca0220bf005ea99ac83eb9ce51301ddb882dcf613ae6cd95fb", + "sha256:96c8ff1e1dd680e38af0887927cab407a4e51d84a5f02ae3d6eb87233036c763", + "sha256:9a7ee79183f0b17dcede8b6723e7da2ded529cf159a878214be9a5d3098f5b1e", + "sha256:a3e2aff8b822ab0e0bdbed9f50494b3a35629c4b9488ae391659973a37a9f53f", + "sha256:a4f3ab9eb8ab2d585ece959c48d234f7b39ac0ca1954a34d8b8e58a52064bdb3", + "sha256:a8b54949267bd5704324397efe9fbb6aa306466dee067550964e994d309db5f1", + "sha256:a96198d5d26f40557d986c1253bfe0e02d18c9d9b93cf389daf1a3c9f7c755fa", + "sha256:aebbd47df77190ada603157f0b3670d578c110c31746ecc5875c394fdcc59a99", + "sha256:af1107299cef049ad00a93df4809517be432283a0847bcae48343ebe5ea340dc", + "sha256:b63465b53baeaf2122a337d4ab57d6bbdd09fcadceb17a974cfa8a0300ad9c67", + "sha256:ba1c779b45a399cc25f511c681016626f69e51e45b9d350d7581998722825af9", + "sha256:bce00f3b1f7f644faae89677ca68645ed5365f1c7f874fdd5ebf730a69640d38", + "sha256:bfdf419bf5d3644f94cd7052954fc233522f5a1b371fc0b00219ebd9c14d5798", + "sha256:c1caa5763d1770216596e0a71b5567f27aac28c95992110212c108ec74589a48", + "sha256:c3e4e1f7b08d1ec6b685ccd3e2d762219c550164fbf524498532e39f9413436e", + "sha256:c85ab016e96a975afbdb9d49ca90f3bca9920ef27c64300843fe91c3d59d8d20", + "sha256:c924deab8105f86980983eced740433fb7554a7f66db73991affa4eda99d5402", + "sha256:d4f818f6371970d6a5d1e42878389bbfb69dcde631e4bbac5ec1cb11158565ca", + "sha256:d920401941cb898ef089422e889759dd403309eb370d0e54f1bdf6ca07fef603", + "sha256:da045bd1147d12bd43fb032296640a7cc17a7f2eaba67495988362e99db24fd2", + "sha256:dc3192a81ecd5ff954cecd690327badd5a84d00b877e1573f7c9097ce13e5bfb", + "sha256:ddae504cfb556fe220efae65e35be63cd11e3c314b202723fc2119ce19f0ca2e", + "sha256:de4544b1fb29cf14870c4e2b8a897c0242449f5dcebd3e0366aa0aa3cf58a23a", + "sha256:dea360778e0668a7ad25d7727d03364de8a45bfd5d808f81253516b9f2217765", + "sha256:e2254fe137c4a360b0a13173a56444f756252c9283ba4d267ca8e9081cd140ea", + "sha256:e64f0421892a207d3780903085c1b04efeb53b16803b23d947de5a7261b71355", + "sha256:e97a29b37830ba1262d8dfd48ddb5b28ad4d3ebecc5d93a9c7591d98641ec737", + "sha256:eacbcf30efaca7dc5cb264228ffecdb95fdb1e715b1ec937c0ce6b734161e0c8", + "sha256:eee5ff934b0c9f4537ff9596169d56cab1890918004791a7a06b879b3ba2a7ef", + "sha256:eff6bac402719c14e17efe845d6b98593c56c843aca6def72080fbede755fd1f", + "sha256:f10954b233d4df5cc3137ffa5ced97f8894152df817e5d149bf05a0ef2ab8134", + "sha256:f23bb1a7a6e8e8b612a164fdd08e683bcc16c76f928d6dbb7bdbee2374fbfee6", + "sha256:f494c01b28645c431239863cb17af8b8d15b93b0d697a0320d5dd34cd9d7c2fa", + "sha256:f6a071d2c3d39b4104f94fc08ab349e9b19b951ad4b8e3b6d7ea92d6ef7ccaf8", + "sha256:f736f54565f8dd7e3ab664fef2bc461d7593a389a7f28d4904af8d55a91bd55f", + "sha256:f8981a94a27ac520a398302afb74ae2c0be1c3d2d215c75c582186a006c9e7b0", + "sha256:fd24996e12e1ba7c397c44be75ca299da14cde34d74bc5508cce233676cc68d0", + "sha256:ff54340fc1129e8e181827e2234af3ff659b4f17d9bbe77f43bc19e6577fadec" + ], + "markers": "python_version >= '3.8'", + "version": "==1.12.1" } }, "develop": { @@ -1791,43 +1790,14 @@ "markers": "python_version >= '3.8'", "version": "==2.3.1" }, - "black": { - "hashes": [ - "sha256:09cdeb74d494ec023ded657f7092ba518e8cf78fa8386155e4a03fdcc44679e6", - "sha256:1f13f7f386f86f8121d76599114bb8c17b69d962137fc70efe56137727c7047e", - "sha256:2500945420b6784c38b9ee885af039f5e7471ef284ab03fa35ecdde4688cd83f", - "sha256:2b59b250fdba5f9a9cd9d0ece6e6d993d91ce877d121d161e4698af3eb9c1018", - "sha256:3c4285573d4897a7610054af5a890bde7c65cb466040c5f0c8b732812d7f0e5e", - "sha256:505289f17ceda596658ae81b61ebbe2d9b25aa78067035184ed0a9d855d18afd", - "sha256:62e8730977f0b77998029da7971fa896ceefa2c4c4933fcd593fa599ecbf97a4", - "sha256:649f6d84ccbae73ab767e206772cc2d7a393a001070a4c814a546afd0d423aed", - "sha256:6e55d30d44bed36593c3163b9bc63bf58b3b30e4611e4d88a0c3c239930ed5b2", - "sha256:707a1ca89221bc8a1a64fb5e15ef39cd755633daa672a9db7498d1c19de66a42", - "sha256:72901b4913cbac8972ad911dc4098d5753704d1f3c56e44ae8dce99eecb0e3af", - "sha256:73bbf84ed136e45d451a260c6b73ed674652f90a2b3211d6a35e78054563a9bb", - "sha256:7c046c1d1eeb7aea9335da62472481d3bbf3fd986e093cffd35f4385c94ae368", - "sha256:81c6742da39f33b08e791da38410f32e27d632260e599df7245cccee2064afeb", - "sha256:837fd281f1908d0076844bc2b801ad2d369c78c45cf800cad7b61686051041af", - "sha256:972085c618ee94f402da1af548a4f218c754ea7e5dc70acb168bfaca4c2542ed", - "sha256:9e84e33b37be070ba135176c123ae52a51f82306def9f7d063ee302ecab2cf47", - "sha256:b19c9ad992c7883ad84c9b22aaa73562a16b819c1d8db7a1a1a49fb7ec13c7d2", - "sha256:d6417535d99c37cee4091a2f24eb2b6d5ec42b144d50f1f2e436d9fe1916fe1a", - "sha256:eab4dd44ce80dea27dc69db40dab62d4ca96112f87996bca68cd75639aeb2e4c", - "sha256:f490dbd59680d809ca31efdae20e634f3fae27fba3ce0ba3208333b713bc3920", - "sha256:fb6e2c0b86bbd43dee042e48059c9ad7830abd5c94b0bc518c0eeec57c3eddc1" - ], - "index": "pypi", - "markers": "python_version >= '3.8'", - "version": "==24.8.0" - }, "boto3": { "hashes": [ - "sha256:97fcc1a14cbc759e4ba9535ced703a99fcf652c9c4b8dfcd06f292c80551684b", - "sha256:be7807f30f26d6c0057e45cfd09dad5968e664488bf4f9138d0bb7a0f6d8ed40" + "sha256:5df4e2cbe3409db07d3a0d8d63d5220ce3202a78206ad87afdbb41519b26ce45", + "sha256:b1cfad301184cdd44dfd4805187ccab12de8dd28dd12a11a5cfdace17918c6de" ], "index": "pypi", "markers": "python_version >= '3.8'", - "version": "==1.35.24" + "version": "==1.35.25" }, "boto3-stubs": { "extras": [ @@ -1838,17 +1808,16 @@ "sha256:55dc1e9b9a6c8456d18bd6747ecf30283d84da4c05d321e2233413b009e2a711", "sha256:cece5d8ed36a5c587bfdcb97a1262678023f1a43c0aad54eeab9f389aefa99ec" ], - "index": "pypi", "markers": "python_version >= '3.8'", "version": "==1.35.25" }, "botocore": { "hashes": [ - "sha256:1e59b0f14f4890c4f70bd6a58a634b9464bed1c4c6171f87c8795d974ade614b", - "sha256:eb9ccc068255cc3d24c36693fda6aec7786db05ae6c2b13bcba66dce6a13e2e3" + "sha256:76c5706b2c6533000603ae8683a297c887abbbaf6ee31e1b2e2863b74b2989bc", + "sha256:e58d60260abf10ccc4417967923117c9902a6a0cff9fddb6ea7ff42dc1bd4630" ], "markers": "python_version >= '3.8'", - "version": "==1.35.24" + "version": "==1.35.25" }, "botocore-stubs": { "hashes": [ @@ -1970,14 +1939,6 @@ "markers": "python_full_version >= '3.7.0'", "version": "==3.3.2" }, - "click": { - "hashes": [ - "sha256:ae74fb96c20a0277a1d615f1e4d73c8414f5a98db8b799a7931d1582f3390c28", - "sha256:ca9853ad459e787e2192211578cc907e7594e294c7ccc834310722b41b9ca6de" - ], - "markers": "python_version >= '3.7'", - "version": "==8.1.7" - }, "coverage": { "hashes": [ "sha256:06a737c882bd26d0d6ee7269b20b12f14a8704807a01056c80bb881a4b2ce6ca", @@ -2138,19 +2099,19 @@ }, "django-stubs": { "hashes": [ - "sha256:78e3764488fdfd2695f12502136548ec22f8d4b1780541a835042b8238d11514", - "sha256:c2502f5ecbae50c68f9a86d52b5b2447d8648fd205036dad0ccb41e19a445927" + "sha256:86128c228b65e6c9a85e5dc56eb1c6f41125917dae0e21e6cfecdf1b27e630c5", + "sha256:b98d49a80aa4adf1433a97407102d068de26c739c405431d93faad96dd282c40" ], "markers": "python_version >= '3.8'", - "version": "==5.0.4" + "version": "==5.1.0" }, "django-stubs-ext": { "hashes": [ - "sha256:85da065224204774208be29c7d02b4482d5a69218a728465c2fbe41725fdc819", - "sha256:910cbaff3d1e8e806a5c27d5ddd4088535aae8371ea921b7fd680fdfa5f14e30" + "sha256:a455fc222c90b30b29ad8c53319559f5b54a99b4197205ddbb385aede03b395d", + "sha256:ed7d51c0b731651879fc75f331fb0806d98b67bfab464e96e2724db6b46ef926" ], "markers": "python_version >= '3.8'", - "version": "==5.0.4" + "version": "==5.1.0" }, "djangorestframework-stubs": { "hashes": [ @@ -2194,15 +2155,6 @@ "markers": "python_version >= '3.8'", "version": "==3.16.1" }, - "flake8": { - "hashes": [ - "sha256:049d058491e228e03e67b390f311bbf88fce2dbaa8fa673e7aea87b7198b8d38", - "sha256:597477df7860daa5aa0fdd84bf5208a043ab96b8e96ab708770ae0364dd03213" - ], - "index": "pypi", - "markers": "python_full_version >= '3.8.1'", - "version": "==7.1.1" - }, "freezegun": { "hashes": [ "sha256:b29dedfcda6d5e8e083ce71b2b542753ad48cfec44037b3fc79702e2980a89e9", @@ -2245,15 +2197,6 @@ "markers": "python_version >= '3.10'", "version": "==8.27.0" }, - "isort": { - "hashes": [ - "sha256:48fdfcb9face5d58a4f6dde2e72a1fb8dcaf8ab26f95ab49fab84c2ddefb0109", - "sha256:8ca5e72a8d85860d5a3fa69b8745237f2939afe12dbf656afbcb47fe72d947a6" - ], - "index": "pypi", - "markers": "python_full_version >= '3.8.0'", - "version": "==5.13.2" - }, "jedi": { "hashes": [ "sha256:cf0496f3651bc65d7174ac1b7d043eff454892c708a87d1b683e57b569927ffd", @@ -2344,14 +2287,6 @@ "markers": "python_version >= '3.8'", "version": "==0.1.7" }, - "mccabe": { - "hashes": [ - "sha256:348e0240c33b60bbdf4e523192ef919f28cb2c3d7d5c7794f74009290f236325", - "sha256:6c2d30ab6be0e4a46919781807b4f0d834ebdd6c6e3dca0bda5a15f863427b6e" - ], - "markers": "python_version >= '3.6'", - "version": "==0.7.0" - }, "mypy": { "hashes": [ "sha256:06d26c277962f3fb50e13044674aa10553981ae514288cb7d0a738f495550b36", @@ -2409,14 +2344,6 @@ "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4, 3.5, 3.6'", "version": "==1.9.1" }, - "packaging": { - "hashes": [ - "sha256:026ed72c8ed3fcce5bf8950572258698927fd1dbda10a5e981cdf0ac37f4f002", - "sha256:5b8f2217dbdbd2f7f384c41c628544e6d52f2d0f53c6d0c3ea61aa5d1d7ff124" - ], - "markers": "python_version >= '3.8'", - "version": "==24.1" - }, "parso": { "hashes": [ "sha256:a418670a20291dacd2dddc80c377c5c3791378ee1e8d12bffc35420643d43f18", @@ -2425,14 +2352,6 @@ "markers": "python_version >= '3.6'", "version": "==0.8.4" }, - "pathspec": { - "hashes": [ - "sha256:a0d503e138a4c123b27490a4f7beda6a01c6f288df0e4a8b79c7eb0dc7b4cc08", - "sha256:a482d51503a1ab33b1c67a6c3813a26953dbdc71c31dacaef9a838c4e29f5712" - ], - "markers": "python_version >= '3.8'", - "version": "==0.12.1" - }, "pexpect": { "hashes": [ "sha256:7236d1e080e4936be2dc3e326cec0af72acf9212a7e1d060210e70a47e253523", @@ -2488,14 +2407,6 @@ "markers": "python_version >= '3.8'", "version": "==2.12.1" }, - "pyflakes": { - "hashes": [ - "sha256:1c61603ff154621fb2a9172037d84dca3500def8c8b630657d1701f026f8af3f", - "sha256:84b5be138a2dfbb40689ca07e2152deb896a65c3a3e24c251c5c62489568074a" - ], - "markers": "python_version >= '3.8'", - "version": "==3.2.0" - }, "pygments": { "hashes": [ "sha256:786ff802f32e91311bff3889f6e9a86e81505fe99f2735bb6d60ae0c5004f199", diff --git a/care/abdm/api/serializers/abha_number.py b/care/abdm/api/serializers/abha_number.py index 33af8c3c18..c166d57228 100644 --- a/care/abdm/api/serializers/abha_number.py +++ b/care/abdm/api/serializers/abha_number.py @@ -4,7 +4,7 @@ from care.abdm.models import AbhaNumber from care.facility.api.serializers.patient import PatientDetailSerializer from care.facility.models import PatientRegistration -from care.utils.serializer.external_id_field import ExternalIdSerializerField +from care.utils.serializers.fields import ExternalIdSerializerField class AbhaNumberSerializer(serializers.ModelSerializer): diff --git a/care/audit_log/helpers.py b/care/audit_log/helpers.py index 04e89814bc..42252ffaa6 100644 --- a/care/audit_log/helpers.py +++ b/care/audit_log/helpers.py @@ -1,3 +1,4 @@ +# ruff: noqa: SLF001 import re from fnmatch import fnmatch from functools import lru_cache @@ -14,7 +15,7 @@ def remove_non_member_fields(d: dict): def instance_finder(v): return isinstance( v, - (list, dict, set), + list | dict | set, ) @@ -41,10 +42,9 @@ class Search(NamedTuple): def _make_search(item): splits = item.split(":") - if len(splits) == 2: + if len(splits) == 2: # noqa: PLR2004 return Search(type=splits[0], value=splits[1]) - else: - return Search(type="plain", value=splits[0]) + return Search(type="plain", value=splits[0]) def candidate_in_scope( @@ -62,7 +62,7 @@ def candidate_in_scope( search_candidate = candidate if is_application: splits = candidate.split(".") - if len(splits) == 2: + if len(splits) == 2: # noqa: PLR2004 app_label, model_name = splits search_candidate = app_label @@ -91,12 +91,11 @@ def exclude_model(model_name): ): return True - if candidate_in_scope( - model_name, settings.AUDIT_LOG["models"]["exclude"]["models"] - ): - return True - - return False + return bool( + candidate_in_scope( + model_name, settings.AUDIT_LOG["models"]["exclude"]["models"] + ) + ) class MetaDataContainer(dict): diff --git a/care/audit_log/middleware.py b/care/audit_log/middleware.py index 35f23efd4b..f2c27d376b 100644 --- a/care/audit_log/middleware.py +++ b/care/audit_log/middleware.py @@ -15,6 +15,7 @@ class RequestInformation(NamedTuple): response: HttpResponse | None exception: Exception | None + logger = logging.getLogger(__name__) @@ -50,7 +51,7 @@ def save(request, response=None, exception=None): if not dal_request_id: dal_request_id = ( f"{request.method.lower()}::" - f"{md5(request.path.lower().encode('utf-8')).hexdigest()}::" + f"{md5(request.path.lower().encode('utf-8')).hexdigest()}::" # noqa: S324 f"{uuid.uuid4().hex}" ) request.dal_request_id = dal_request_id @@ -69,8 +70,7 @@ def get_current_user(): environ = RequestInformation(*AuditLogMiddleware.thread.__dal__) if isinstance(environ.request.user, AnonymousUser): return None - else: - return environ.request.user + return environ.request.user @staticmethod def get_current_request(): @@ -85,14 +85,14 @@ def __call__(self, request: HttpRequest): response: HttpResponse = self.get_response(request) self.save(request, response) - if request.user: - current_user_str = f"{request.user.id}|{request.user}" - else: - current_user_str = None + current_user_str = f"{request.user.id}|{request.user}" if request.user else None logger.info( - f"{request.method} {request.path} {response.status_code} " - f"User:[{current_user_str}]" + "%s %s %s User:[%s]", + request.method, + request.path, + response.status_code, + current_user_str, ) return response diff --git a/care/audit_log/receivers.py b/care/audit_log/receivers.py index 9b812c42a0..95c28d1abb 100644 --- a/care/audit_log/receivers.py +++ b/care/audit_log/receivers.py @@ -1,3 +1,4 @@ +# ruff: noqa: SLF001 import json import logging from typing import NamedTuple @@ -22,6 +23,7 @@ logger = logging.getLogger(__name__) + class Event(NamedTuple): model: str actor: AbstractUser @@ -42,7 +44,7 @@ def pre_save_signal(sender, instance, **kwargs) -> None: model_name = get_model_name(instance) if exclude_model(model_name): - logger.debug(f"{model_name} ignored as per settings") + logger.debug("%s ignored as per settings", model_name) return get_or_create_meta(instance) @@ -61,8 +63,9 @@ def pre_save_signal(sender, instance, **kwargs) -> None: changes = {} if operation not in {Operation.INSERT, Operation.DELETE}: - old, new = remove_non_member_fields(pre.__dict__), remove_non_member_fields( - instance.__dict__ + old, new = ( + remove_non_member_fields(pre.__dict__), + remove_non_member_fields(instance.__dict__), ) try: @@ -107,7 +110,7 @@ def _post_processor(instance, event: Event | None, operation: Operation): model_name = get_model_name(instance) if not event and operation != Operation.DELETE: - logger.debug(f"Event not received for {operation}. Ignoring.") + logger.debug("Event not received for %s. Ignoring.", operation) return try: @@ -118,11 +121,17 @@ def _post_processor(instance, event: Event | None, operation: Operation): else: changes = json.dumps(event.changes if event else {}, cls=LogJsonEncoder) except Exception: - logger.warning(f"Failed to log {event}", exc_info=True) + logger.warning("Failed to log %s", event, exc_info=True) return logger.info( - f"AUDIT_LOG::{request_id}|{actor}|{operation.value}|{model_name}|ID:{instance.pk}|{changes}" + "AUDIT_LOG::%s|%s|%s|%s|ID:%s|%s", + request_id, + actor, + operation.value, + model_name, + instance.pk, + changes, ) @@ -137,7 +146,7 @@ def post_save_signal(sender, instance, created, update_fields: frozenset, **kwar model_name = get_model_name(instance) if exclude_model(model_name): - logger.debug(f"Ignoring {model_name}.") + logger.debug("Ignoring %s.", model_name) return operation = Operation.INSERT if created else Operation.UPDATE @@ -158,7 +167,7 @@ def post_delete_signal(sender, instance, **kwargs) -> None: model_name = get_model_name(instance) if exclude_model(model_name): - logger.debug(f"Ignoring {model_name}.") + logger.debug("Ignoring %s.", model_name) return event = instance._meta.dal.event diff --git a/care/facility/admin.py b/care/facility/admin.py index 55a9bdff65..779a313acb 100644 --- a/care/facility/admin.py +++ b/care/facility/admin.py @@ -55,7 +55,7 @@ class DistrictFilter(SimpleListFilter): def lookups(self, request, model_admin): district = Facility.objects.values_list("district__name", flat=True) - return list(map(lambda x: (x, x), set(district))) + return [(x, x) for x in set(district)] def queryset(self, request, queryset): if self.value() is None: @@ -63,22 +63,6 @@ def queryset(self, request, queryset): return queryset.filter(district__name=self.value()) -# class LocalBodyFilter(SimpleListFilter): -# """Local body filter""" - -# title = "Local body" -# parameter_name = "local_body" - -# def lookups(self, request, model_admin): -# local_body = Facility.objects.values_list("local_body__name", flat=True) -# return list(map(lambda x: (x, x), set(local_body))) - -# def queryset(self, request, queryset): -# if self.value() is None: -# return queryset -# return queryset.filter(local_body__name=self.value()) - - class StateFilter(SimpleListFilter): """State filter""" @@ -87,7 +71,7 @@ class StateFilter(SimpleListFilter): def lookups(self, request, model_admin): state = Facility.objects.values_list("state__name", flat=True) - return list(map(lambda x: (x, x), set(state))) + return [(x, x) for x in set(state)] def queryset(self, request, queryset): if self.value() is None: @@ -222,7 +206,7 @@ class FacilityFeatureFlagForm(forms.ModelForm): ) class Meta: - fields = "__all__" + fields = ("flag", "facility") model = FacilityFlag form = FacilityFeatureFlagForm diff --git a/care/facility/api/serializers/ambulance.py b/care/facility/api/serializers/ambulance.py index 1668922158..7cdbf13922 100644 --- a/care/facility/api/serializers/ambulance.py +++ b/care/facility/api/serializers/ambulance.py @@ -10,7 +10,7 @@ class AmbulanceDriverSerializer(serializers.ModelSerializer): class Meta: model = AmbulanceDriver - exclude = TIMESTAMP_FIELDS + ("ambulance",) + exclude = (*TIMESTAMP_FIELDS, "ambulance") class AmbulanceSerializer(serializers.ModelSerializer): @@ -36,9 +36,8 @@ class Meta: def validate(self, obj): validated = super().validate(obj) if not validated.get("price_per_km") and not validated.get("has_free_service"): - raise ValidationError( - "The ambulance must provide a price or be marked as free" - ) + msg = "The ambulance must provide a price or be marked as free" + raise ValidationError(msg) return validated def create(self, validated_data): @@ -46,7 +45,7 @@ def create(self, validated_data): drivers = validated_data.pop("drivers", []) validated_data.pop("created_by", None) - ambulance = super(AmbulanceSerializer, self).create(validated_data) + ambulance = super().create(validated_data) for d in drivers: d["ambulance"] = ambulance @@ -55,8 +54,7 @@ def create(self, validated_data): def update(self, instance, validated_data): validated_data.pop("drivers", []) - ambulance = super(AmbulanceSerializer, self).update(instance, validated_data) - return ambulance + return super().update(instance, validated_data) class DeleteDriverSerializer(serializers.Serializer): diff --git a/care/facility/api/serializers/asset.py b/care/facility/api/serializers/asset.py index c1543d73c4..f403361e1a 100644 --- a/care/facility/api/serializers/asset.py +++ b/care/facility/api/serializers/asset.py @@ -1,5 +1,3 @@ -from datetime import datetime - from django.core.cache import cache from django.db import models, transaction from django.db.models import F, Value @@ -38,9 +36,9 @@ from care.utils.assetintegration.hl7monitor import HL7MonitorAsset from care.utils.assetintegration.onvif import OnvifAsset from care.utils.assetintegration.ventilator import VentilatorAsset +from care.utils.models.validators import MiddlewareDomainAddressValidator from care.utils.queryset.facility import get_facility_queryset -from config.serializers import ChoiceField -from config.validators import MiddlewareDomainAddressValidator +from care.utils.serializers.fields import ChoiceField class AssetLocationSerializer(ModelSerializer): @@ -125,9 +123,7 @@ def update(self, instance, validated_data): ) edit.save() - updated_instance = super().update(instance, validated_data) - - return updated_instance + return super().update(instance, validated_data) @extend_schema_field( @@ -159,10 +155,7 @@ class AssetSerializer(ModelSerializer): class Meta: model = Asset exclude = ("deleted", "external_id", "current_location") - read_only_fields = TIMESTAMP_FIELDS + ( - "resolved_middleware", - "latest_status", - ) + read_only_fields = (*TIMESTAMP_FIELDS, "resolved_middleware", "latest_status") def validate_qr_code_id(self, value): value = value or None # treat empty string as null @@ -195,15 +188,14 @@ def validate(self, attrs): ): del attrs["warranty_amc_end_of_validity"] - elif warranty_amc_end_of_validity < datetime.now().date(): - raise ValidationError( - "Warranty/AMC end of validity cannot be in the past" - ) + elif warranty_amc_end_of_validity < now().date(): + msg = "Warranty/AMC end of validity cannot be in the past" + raise ValidationError(msg) # validate that last serviced date is not in the future - if attrs.get("last_serviced_on"): - if attrs["last_serviced_on"] > datetime.now().date(): - raise ValidationError("Last serviced on cannot be in the future") + if attrs.get("last_serviced_on") and attrs["last_serviced_on"] > now().date(): + msg = "Last serviced on cannot be in the future" + raise ValidationError(msg) # only allow setting asset class on creation (or updation if asset class is not set) if ( @@ -250,9 +242,8 @@ def validate(self, attrs): .first() ) if asset_using_ip: - raise ValidationError( - f"IP Address {ip_address} is already in use by {asset_using_ip.name} asset" - ) + msg = f"IP Address {ip_address} is already in use by {asset_using_ip.name} asset" + raise ValidationError(msg) return super().validate(attrs) @@ -361,7 +352,7 @@ def to_representation(self, instance: Asset): data["ip_address"] = instance.meta.get("local_ip_address") if camera_access_key := instance.meta.get("camera_access_key"): values = camera_access_key.split(":") - if len(values) == 3: + if len(values) == 3: # noqa: PLR2004 data["username"], data["password"], data["access_key"] = values return data @@ -416,7 +407,7 @@ class Meta: class AssetActionSerializer(Serializer): - def actionChoices(): + def action_choices(): actions = [ OnvifAsset.OnvifActions, HL7MonitorAsset.HL7MonitorActions, @@ -428,7 +419,7 @@ def actionChoices(): return choices type = ChoiceField( - choices=actionChoices(), + choices=action_choices(), required=True, ) data = JSONField(required=False) diff --git a/care/facility/api/serializers/bed.py b/care/facility/api/serializers/bed.py index ea66efd499..41597e186d 100644 --- a/care/facility/api/serializers/bed.py +++ b/care/facility/api/serializers/bed.py @@ -30,8 +30,7 @@ from care.utils.assetintegration.asset_classes import AssetClasses from care.utils.queryset.consultation import get_consultation_queryset from care.utils.queryset.facility import get_facility_queryset -from care.utils.serializer.external_id_field import ExternalIdSerializerField -from config.serializers import ChoiceField +from care.utils.serializers.fields import ChoiceField, ExternalIdSerializerField class BedSerializer(ModelSerializer): @@ -51,8 +50,10 @@ def validate_name(self, value): return value.strip() if value else value def validate_number_of_beds(self, value): - if value > 100: - raise ValidationError("Cannot create more than 100 beds at once.") + max_beds = 100 + if value > max_beds: + msg = f"Cannot create more than {max_beds} beds at once." + raise ValidationError(msg) return value class Meta: @@ -152,10 +153,11 @@ def get_patient(self, obj): ).first() if patient: return PatientListSerializer(patient).data + return None class Meta: model = AssetBed - exclude = ("external_id", "id") + TIMESTAMP_FIELDS + exclude = ("external_id", "id", *TIMESTAMP_FIELDS) class ConsultationBedSerializer(ModelSerializer): @@ -179,7 +181,7 @@ class Meta: exclude = ("deleted", "external_id") read_only_fields = TIMESTAMP_FIELDS - def validate(self, attrs): + def validate(self, attrs): # noqa: PLR0912 if "consultation" not in attrs: raise ValidationError({"consultation": "This field is required."}) if "bed" not in attrs: @@ -192,7 +194,8 @@ def validate(self, attrs): facilities = get_facility_queryset(user) if not facilities.filter(id=bed.facility_id).exists(): - raise ValidationError("You do not have access to this facility") + msg = "You do not have access to this facility" + raise ValidationError(msg) permitted_consultations = get_consultation_queryset(user).select_related( "patient" @@ -205,17 +208,20 @@ def validate(self, attrs): or consultation.discharge_date or consultation.death_datetime ): - raise ValidationError("Patient not active") + msg = "Patient not active" + raise ValidationError(msg) # bed validations if consultation.facility_id != bed.facility_id: - raise ValidationError("Consultation and bed are not in the same facility") + msg = "Consultation and bed are not in the same facility" + raise ValidationError(msg) if ( ConsultationBed.objects.filter(bed=bed, end_date__isnull=True) .exclude(consultation=consultation) .exists() ): - raise ValidationError("Bed is already in use") + msg = "Bed is already in use" + raise ValidationError(msg) # check whether the same set of bed and assets are already assigned current_consultation_bed = consultation.current_bed @@ -230,7 +236,8 @@ def validate(self, attrs): ) == set(attrs.get("assets", [])) ): - raise ValidationError("These set of bed and assets are already assigned") + msg = "These set of bed and assets are already assigned" + raise ValidationError(msg) # date validations # note: end_date is for setting end date on current instance @@ -323,10 +330,11 @@ def create(self, validated_data) -> ConsultationBed: ) not_found_assets = set(assets_ids) - set(assets) if not_found_assets: - raise ValidationError( + msg = ( "Some assets are not available - " f"{' ,'.join([str(x) for x in not_found_assets])}" ) + raise ValidationError(msg) obj: ConsultationBed = super().create(validated_data) if assets_ids: asset_objects = Asset.objects.filter(external_id__in=assets_ids).only( diff --git a/care/facility/api/serializers/consultation_diagnosis.py b/care/facility/api/serializers/consultation_diagnosis.py index be0e64676f..a6ccd6cd4c 100644 --- a/care/facility/api/serializers/consultation_diagnosis.py +++ b/care/facility/api/serializers/consultation_diagnosis.py @@ -14,7 +14,8 @@ class ConsultationCreateDiagnosisSerializer(serializers.ModelSerializer): def validate_verification_status(self, value): if value in INACTIVE_CONDITION_VERIFICATION_STATUSES: - raise serializers.ValidationError("Verification status not allowed") + msg = "Verification status not allowed" + raise serializers.ValidationError(msg) return value class Meta: @@ -54,7 +55,8 @@ def get_consultation_external_id(self): def validate_diagnosis(self, value): if self.instance and value != self.instance.diagnosis: - raise serializers.ValidationError("Diagnosis cannot be changed") + msg = "Diagnosis cannot be changed" + raise serializers.ValidationError(msg) if ( not self.instance @@ -63,15 +65,15 @@ def validate_diagnosis(self, value): diagnosis=value, ).exists() ): - raise serializers.ValidationError( - "Diagnosis already exists for consultation" - ) + msg = "Diagnosis already exists for consultation" + raise serializers.ValidationError(msg) return value def validate_verification_status(self, value): if not self.instance and value in INACTIVE_CONDITION_VERIFICATION_STATUSES: - raise serializers.ValidationError("Verification status not allowed") + msg = "Verification status not allowed" + raise serializers.ValidationError(msg) return value def validate_is_principal(self, value): @@ -87,9 +89,8 @@ def validate_is_principal(self, value): qs = qs.exclude(id=self.instance.id) if qs.exists(): - raise serializers.ValidationError( - "Consultation already has a principal diagnosis. Unset the existing principal diagnosis first." - ) + msg = "Consultation already has a principal diagnosis. Unset the existing principal diagnosis first." + raise serializers.ValidationError(msg) return value diff --git a/care/facility/api/serializers/daily_round.py b/care/facility/api/serializers/daily_round.py index 1f4639ec0d..92d06c3c5c 100644 --- a/care/facility/api/serializers/daily_round.py +++ b/care/facility/api/serializers/daily_round.py @@ -1,4 +1,5 @@ from datetime import timedelta +from typing import TYPE_CHECKING from django.db import transaction from django.utils import timezone @@ -16,11 +17,13 @@ from care.facility.models.daily_round import DailyRound from care.facility.models.notification import Notification from care.facility.models.patient_base import SuggestionChoices -from care.facility.models.patient_consultation import PatientConsultation from care.users.api.serializers.user import UserBaseMinimumSerializer from care.utils.notification_handler import NotificationGenerator from care.utils.queryset.facility import get_home_facility_queryset -from config.serializers import ChoiceField +from care.utils.serializers.fields import ChoiceField + +if TYPE_CHECKING: + from care.facility.models.patient_consultation import PatientConsultation class DailyRoundSerializer(serializers.ModelSerializer): @@ -102,8 +105,6 @@ class DailyRoundSerializer(serializers.ModelSerializer): last_edited_by = UserBaseMinimumSerializer(read_only=True) created_by = UserBaseMinimumSerializer(read_only=True) - # bed_object = BedSerializer(read_only=True) - class Meta: model = DailyRound read_only_fields = ( @@ -120,7 +121,8 @@ def validate_bp(self, value): if value is not None: sys, dia = value.get("systolic"), value.get("diastolic") if sys is not None and dia is not None and sys < dia: - raise ValidationError("Systolic must be greater than diastolic") + msg = "Systolic must be greater than diastolic" + raise ValidationError(msg) return value def update(self, instance, validated_data): @@ -295,24 +297,22 @@ def validate(self, attrs): {"consultation": ["Discharged Consultation data cannot be updated"]} ) - if "action" in validated: - if validated["action"] == PatientRegistration.ActionEnum.REVIEW: - if "consultation__review_interval" not in validated: - raise ValidationError( - { - "review_interval": [ - "This field is required as the patient has been requested Review." - ] - } - ) - if validated["consultation__review_interval"] <= 0: - raise ValidationError( - { - "review_interval": [ - "This field value is must be greater than 0." - ] - } - ) + if ( + "action" in validated + and validated["action"] == PatientRegistration.ActionEnum.REVIEW + ): + if "consultation__review_interval" not in validated: + raise ValidationError( + { + "review_interval": [ + "This field is required as the patient has been requested Review." + ] + } + ) + if validated["consultation__review_interval"] <= 0: + raise ValidationError( + {"review_interval": ["This field value is must be greater than 0."]} + ) if "bed" in validated: external_id = validated.pop("bed")["external_id"] @@ -327,5 +327,6 @@ def validate(self, attrs): def validate_taken_at(self, value): if value and value > timezone.now(): - raise serializers.ValidationError("Cannot create an update in the future") + msg = "Cannot create an update in the future" + raise serializers.ValidationError(msg) return value diff --git a/care/facility/api/serializers/encounter_symptom.py b/care/facility/api/serializers/encounter_symptom.py index 858ab7f9c8..d669dd0aab 100644 --- a/care/facility/api/serializers/encounter_symptom.py +++ b/care/facility/api/serializers/encounter_symptom.py @@ -33,7 +33,8 @@ class Meta: def validate_onset_date(self, value): if value and value > now(): - raise serializers.ValidationError("Onset date cannot be in the future") + msg = "Onset date cannot be in the future" + raise serializers.ValidationError(msg) return value def validate(self, attrs): @@ -49,11 +50,10 @@ def validate(self, attrs): if self.instance else validated_data.get("onset_date") ) - if cure_date := validated_data.get("cure_date"): - if cure_date < onset_date: - raise serializers.ValidationError( - {"cure_date": "Cure date should be after onset date"} - ) + if validated_data.get("cure_date") and validated_data["cure_date"] < onset_date: + raise serializers.ValidationError( + {"cure_date": "Cure date should be after onset date"} + ) if validated_data.get("symptom") != Symptom.OTHERS and validated_data.get( "other_symptom" diff --git a/care/facility/api/serializers/facility.py b/care/facility/api/serializers/facility.py index 92dea0870a..bd42b9bba4 100644 --- a/care/facility/api/serializers/facility.py +++ b/care/facility/api/serializers/facility.py @@ -14,12 +14,11 @@ ) from care.utils.file_uploads.cover_image import upload_cover_image from care.utils.models.validators import ( + MiddlewareDomainAddressValidator, cover_image_validator, custom_image_extension_validator, ) -from care.utils.serializer.external_id_field import ExternalIdSerializerField -from config.serializers import ChoiceField -from config.validators import MiddlewareDomainAddressValidator +from care.utils.serializers.fields import ChoiceField, ExternalIdSerializerField User = get_user_model() @@ -97,12 +96,7 @@ class FacilitySerializer(FacilityBasicInfoSerializer): """Serializer for facility.models.Facility.""" facility_type = ChoiceField(choices=FACILITY_TYPES) - # A valid location => { - # "latitude": 49.8782482189424, - # "longitude": 24.452545489 - # } read_cover_image_url = serializers.URLField(read_only=True) - # location = PointField(required=False) features = serializers.ListField( child=serializers.ChoiceField(choices=FEATURE_CHOICES), required=False, @@ -155,7 +149,8 @@ class Meta: def validate_middleware_address(self, value): if not value: - raise serializers.ValidationError("Middleware Address is required") + msg = "Middleware Address is required" + raise serializers.ValidationError(msg) value = value.strip() if not value: return value @@ -166,9 +161,8 @@ def validate_middleware_address(self, value): def validate_features(self, value): if len(value) != len(set(value)): - raise serializers.ValidationError( - "Features should not contain duplicate values." - ) + msg = "Features should not contain duplicate values." + raise serializers.ValidationError(msg) return value def create(self, validated_data): @@ -211,12 +205,14 @@ def validate_spoke(self, spoke: Facility): hub: Facility = self.context["facility"] if hub == spoke: - raise serializers.ValidationError("Cannot set a facility as it's own spoke") + msg = "Cannot set a facility as it's own spoke" + raise serializers.ValidationError(msg) if FacilityHubSpoke.objects.filter( Q(hub=hub, spoke=spoke) | Q(hub=spoke, spoke=hub) ).first(): - raise serializers.ValidationError("Facility is already a spoke/hub") + msg = "Facility is already a spoke/hub" + raise serializers.ValidationError(msg) return spoke diff --git a/care/facility/api/serializers/facility_capacity.py b/care/facility/api/serializers/facility_capacity.py index 4db943c69c..983dbe1a94 100644 --- a/care/facility/api/serializers/facility_capacity.py +++ b/care/facility/api/serializers/facility_capacity.py @@ -2,7 +2,7 @@ from care.facility.api.serializers import TIMESTAMP_FIELDS from care.facility.models import FacilityCapacity, RoomType -from config.serializers import ChoiceField +from care.utils.serializers.fields import ChoiceField class FacilityCapacitySerializer(serializers.ModelSerializer): @@ -44,4 +44,4 @@ def __init__(self, model, *args, **kwargs): super().__init__() class Meta: - exclude = TIMESTAMP_FIELDS + ("facility",) + exclude = (*TIMESTAMP_FIELDS, "facility") diff --git a/care/facility/api/serializers/file_upload.py b/care/facility/api/serializers/file_upload.py index 6afc7d8434..9f451aaf73 100644 --- a/care/facility/api/serializers/file_upload.py +++ b/care/facility/api/serializers/file_upload.py @@ -16,10 +16,10 @@ from care.users.api.serializers.user import UserBaseMinimumSerializer from care.users.models import User from care.utils.notification_handler import NotificationGenerator -from config.serializers import ChoiceField +from care.utils.serializers.fields import ChoiceField -def check_permissions(file_type, associating_id, user, action="create"): +def check_permissions(file_type, associating_id, user, action="create"): # noqa: PLR0911, PLR0912 try: if file_type == FileUpload.FileType.PATIENT.value: patient = PatientRegistration.objects.get(external_id=associating_id) @@ -27,17 +27,19 @@ def check_permissions(file_type, associating_id, user, action="create"): raise serializers.ValidationError( {"patient": "Cannot upload file for a discharged patient."} ) - if patient.assigned_to: - if user == patient.assigned_to: - return patient.id - if patient.last_consultation: - if patient.last_consultation.assigned_to: - if user == patient.last_consultation.assigned_to: - return patient.id + if patient.assigned_to and user == patient.assigned_to: + return patient.id + if ( + patient.last_consultation + and patient.last_consultation.assigned_to + and user == patient.last_consultation.assigned_to + ): + return patient.id if not has_facility_permission(user, patient.facility): - raise Exception("No Permission") + msg = "No Permission" + raise Exception(msg) return patient.id - elif file_type == FileUpload.FileType.CONSULTATION.value: + if file_type == FileUpload.FileType.CONSULTATION.value: consultation = PatientConsultation.objects.get(external_id=associating_id) if consultation.discharge_date and not action == "read": raise serializers.ValidationError( @@ -45,19 +47,21 @@ def check_permissions(file_type, associating_id, user, action="create"): "consultation": "Cannot upload file for a discharged consultation." } ) - if consultation.patient.assigned_to: - if user == consultation.patient.assigned_to: - return consultation.id - if consultation.assigned_to: - if user == consultation.assigned_to: - return consultation.id + if ( + consultation.patient.assigned_to + and user == consultation.patient.assigned_to + ): + return consultation.id + if consultation.assigned_to and user == consultation.assigned_to: + return consultation.id if not ( has_facility_permission(user, consultation.patient.facility) or has_facility_permission(user, consultation.facility) ): - raise Exception("No Permission") + msg = "No Permission" + raise Exception(msg) return consultation.id - elif file_type == FileUpload.FileType.CONSENT_RECORD.value: + if file_type == FileUpload.FileType.CONSENT_RECORD.value: consultation = PatientConsent.objects.get( external_id=associating_id ).consultation @@ -68,14 +72,14 @@ def check_permissions(file_type, associating_id, user, action="create"): } ) if ( - user == consultation.assigned_to - or user == consultation.patient.assigned_to + user in (consultation.assigned_to, consultation.patient.assigned_to) or has_facility_permission(user, consultation.facility) or has_facility_permission(user, consultation.patient.facility) ): return associating_id - raise Exception("No Permission") - elif file_type == FileUpload.FileType.DISCHARGE_SUMMARY.value: + msg = "No Permission" + raise Exception(msg) + if file_type == FileUpload.FileType.DISCHARGE_SUMMARY.value: consultation = PatientConsultation.objects.get(external_id=associating_id) if ( consultation.patient.assigned_to @@ -88,36 +92,39 @@ def check_permissions(file_type, associating_id, user, action="create"): has_facility_permission(user, consultation.patient.facility) or has_facility_permission(user, consultation.facility) ): - raise Exception("No Permission") + msg = "No Permission" + raise Exception(msg) return consultation.external_id - elif file_type == FileUpload.FileType.SAMPLE_MANAGEMENT.value: + if file_type == FileUpload.FileType.SAMPLE_MANAGEMENT.value: sample = PatientSample.objects.get(external_id=associating_id) patient = sample.patient - if patient.assigned_to: - if user == patient.assigned_to: - return sample.id - if sample.consultation: - if sample.consultation.assigned_to: - if user == sample.consultation.assigned_to: - return sample.id - if sample.testing_facility: - if has_facility_permission( - user, - Facility.objects.get( - external_id=sample.testing_facility.external_id - ), - ): - return sample.id + if patient.assigned_to and user == patient.assigned_to: + return sample.id + if ( + sample.consultation + and sample.consultation.assigned_to + and user == sample.consultation.assigned_to + ): + return sample.id + if sample.testing_facility and has_facility_permission( + user, + Facility.objects.get(external_id=sample.testing_facility.external_id), + ): + return sample.id if not has_facility_permission(user, patient.facility): - raise Exception("No Permission") + msg = "No Permission" + raise Exception(msg) return sample.id - elif file_type == FileUpload.FileType.CLAIM.value or file_type == FileUpload.FileType.COMMUNICATION.value: + if file_type in ( + FileUpload.FileType.CLAIM.value, + FileUpload.FileType.COMMUNICATION.value, + ): return associating_id - else: - raise Exception("Undefined File Type") + msg = "Undefined File Type" + raise Exception(msg) - except Exception: - raise serializers.ValidationError({"permission": "denied"}) + except Exception as e: + raise serializers.ValidationError({"permission": "denied"}) from e class FileUploadCreateSerializer(serializers.ModelSerializer): @@ -248,7 +255,8 @@ def update(self, instance, validated_data): def validate(self, attrs): validated = super().validate(attrs) if validated.get("is_archived") and not validated.get("archive_reason"): - raise ValidationError("Archive reason must be specified.") + msg = "Archive reason must be specified." + raise ValidationError(msg) return validated diff --git a/care/facility/api/serializers/hospital_doctor.py b/care/facility/api/serializers/hospital_doctor.py index 540a46a38d..e53901c7bf 100644 --- a/care/facility/api/serializers/hospital_doctor.py +++ b/care/facility/api/serializers/hospital_doctor.py @@ -2,7 +2,7 @@ from care.facility.api.serializers import TIMESTAMP_FIELDS from care.facility.models import DOCTOR_TYPES, HospitalDoctors -from config.serializers import ChoiceField +from care.utils.serializers.fields import ChoiceField class HospitalDoctorSerializer(serializers.ModelSerializer): @@ -15,4 +15,4 @@ class Meta: "id", "area_text", ) - exclude = TIMESTAMP_FIELDS + ("facility", "external_id") + exclude = (*TIMESTAMP_FIELDS, "facility", "external_id") diff --git a/care/facility/api/serializers/inventory.py b/care/facility/api/serializers/inventory.py index e035137a5c..2172e19e24 100644 --- a/care/facility/api/serializers/inventory.py +++ b/care/facility/api/serializers/inventory.py @@ -68,10 +68,10 @@ def create(self, validated_data): try: item.allowed_units.get(id=unit.id) - except FacilityInventoryUnit.DoesNotExist: + except FacilityInventoryUnit.DoesNotExist as e: raise serializers.ValidationError( {"unit": ["Item cannot be measured with unit"]} - ) + ) from e multiplier = 1 @@ -80,10 +80,10 @@ def create(self, validated_data): multiplier = FacilityInventoryUnitConverter.objects.get( from_unit=unit, to_unit=item.default_unit ).multiplier - except FacilityInventoryUnitConverter.DoesNotExist: + except FacilityInventoryUnitConverter.DoesNotExist as e: raise serializers.ValidationError( {"item": ["Please Ask Admin to Add Conversion Metrics"]} - ) + ) from e validated_data["created_by"] = self.context["request"].user @@ -197,10 +197,10 @@ def create(self, validated_data): try: instance = super().create(validated_data) - except IntegrityError: + except IntegrityError as e: raise serializers.ValidationError( {"item": ["Item min quantity already set"]} - ) + ) from e try: summary_obj = FacilityInventorySummary.objects.get( @@ -214,9 +214,8 @@ def create(self, validated_data): return instance def update(self, instance, validated_data): - if "item" in validated_data: - if instance.item != validated_data["item"]: - raise serializers.ValidationError({"item": ["Item cannot be Changed"]}) + if "item" in validated_data and instance.item != validated_data["item"]: + raise serializers.ValidationError({"item": ["Item cannot be Changed"]}) item = validated_data["item"] diff --git a/care/facility/api/serializers/notification.py b/care/facility/api/serializers/notification.py index 7d7d424f87..e0fcd2b535 100644 --- a/care/facility/api/serializers/notification.py +++ b/care/facility/api/serializers/notification.py @@ -2,7 +2,7 @@ from care.facility.models.notification import Notification from care.users.api.serializers.user import UserBaseMinimumSerializer -from config.serializers import ChoiceField +from care.utils.serializers.fields import ChoiceField class NotificationSerializer(serializers.ModelSerializer): diff --git a/care/facility/api/serializers/patient.py b/care/facility/api/serializers/patient.py index 7214be8d77..926e9b21bb 100644 --- a/care/facility/api/serializers/patient.py +++ b/care/facility/api/serializers/patient.py @@ -1,8 +1,6 @@ -import datetime - from django.conf import settings from django.db import transaction -from django.utils.timezone import make_aware, now +from django.utils.timezone import now from rest_framework import serializers from care.facility.api.serializers import TIMESTAMP_FIELDS @@ -46,8 +44,7 @@ from care.users.models import User from care.utils.notification_handler import NotificationGenerator from care.utils.queryset.facility import get_home_facility_queryset -from care.utils.serializer.external_id_field import ExternalIdSerializerField -from config.serializers import ChoiceField +from care.utils.serializers.fields import ChoiceField, ExternalIdSerializerField class PatientMetaInfoSerializer(serializers.ModelSerializer): @@ -102,7 +99,7 @@ class Meta: "allergies", "external_id", ) - read_only = TIMESTAMP_FIELDS + ("death_datetime",) + read_only = (*TIMESTAMP_FIELDS, "death_datetime") class PatientContactDetailsSerializer(serializers.ModelSerializer): @@ -144,9 +141,6 @@ class MedicalHistorySerializer(serializers.Serializer): last_consultation = PatientConsultationSerializer(read_only=True) facility_object = FacilitySerializer(source="facility", read_only=True) - # nearest_facility_object = FacilitySerializer( - # source="nearest_facility", read_only=True - # ) source = ChoiceField( choices=PatientRegistration.SourceChoices, @@ -170,7 +164,7 @@ class MedicalHistorySerializer(serializers.Serializer): last_edited = UserBaseMinimumSerializer(read_only=True) created_by = UserBaseMinimumSerializer(read_only=True) vaccine_name = serializers.ChoiceField( - choices=PatientRegistration.vaccineChoices, required=False, allow_null=True + choices=PatientRegistration.VaccineChoices, required=False, allow_null=True ) assigned_to_object = UserBaseMinimumSerializer(source="assigned_to", read_only=True) @@ -189,24 +183,14 @@ class Meta: "external_id", ) include = ("contacted_patients",) - read_only = TIMESTAMP_FIELDS + ( + read_only = ( + *TIMESTAMP_FIELDS, "last_edited", "created_by", "is_active", "death_datetime", ) - # def get_last_consultation(self, obj): - # last_consultation = PatientConsultation.objects.filter(patient=obj).last() - # if not last_consultation: - # return None - # return PatientConsultationSerializer(last_consultation).data - - # def validate_facility(self, value): - # if value is not None and Facility.objects.filter(external_id=value).first() is None: - # raise serializers.ValidationError("facility not found") - # return value - def validate_countries_travelled(self, value): if not value: value = [] @@ -216,12 +200,14 @@ def validate_countries_travelled(self, value): def validate_date_of_birth(self, value): if value and value > now().date(): - raise serializers.ValidationError("Enter a valid DOB such that age > 0") + msg = "Enter a valid DOB such that age > 0" + raise serializers.ValidationError(msg) return value def validate_year_of_birth(self, value): if value and value > now().year: - raise serializers.ValidationError("Enter a valid year of birth") + msg = "Enter a valid year of birth" + raise serializers.ValidationError(msg) return value def validate(self, attrs): @@ -239,9 +225,11 @@ def validate(self, attrs): if validated.get("is_vaccinated"): if validated.get("number_of_doses") == 0: - raise serializers.ValidationError("Number of doses cannot be 0") + msg = "Number of doses cannot be 0" + raise serializers.ValidationError(msg) if validated.get("vaccine_name") is None: - raise serializers.ValidationError("Vaccine name cannot be null") + msg = "Vaccine name cannot be null" + raise serializers.ValidationError(msg) return validated @@ -276,9 +264,8 @@ def create(self, validated_data): # Authorisation checks end - if "srf_id" in validated_data: - if validated_data["srf_id"]: - self.check_external_entry(validated_data["srf_id"]) + if validated_data.get("srf_id"): + self.check_external_entry(validated_data["srf_id"]) validated_data["created_by"] = self.context["request"].user patient = super().create(validated_data) @@ -325,9 +312,11 @@ def update(self, instance, validated_data): external_id=external_id ).id - if "srf_id" in validated_data: - if instance.srf_id != validated_data["srf_id"]: - self.check_external_entry(validated_data["srf_id"]) + if ( + "srf_id" in validated_data + and instance.srf_id != validated_data["srf_id"] + ): + self.check_external_entry(validated_data["srf_id"]) patient = super().update(instance, validated_data) Disease.objects.filter(patient=patient).update(deleted=True) @@ -371,9 +360,7 @@ def update(self, instance, validated_data): class FacilityPatientStatsHistorySerializer(serializers.ModelSerializer): id = serializers.CharField(source="external_id", read_only=True) - entry_date = serializers.DateField( - default=make_aware(datetime.datetime.today()).date() - ) + entry_date = serializers.DateField(default=lambda: now().date()) facility = ExternalIdSerializerField( queryset=Facility.objects.all(), read_only=True ) @@ -430,7 +417,8 @@ class Meta: def validate_year_of_birth(self, value): if self.instance and self.instance.year_of_birth != value: - raise serializers.ValidationError("Year of birth does not match") + msg = "Year of birth does not match" + raise serializers.ValidationError(msg) return value def create(self, validated_data): @@ -531,13 +519,11 @@ def create(self, validated_data): if validated_data.get("reply_to"): reply_to_note = validated_data["reply_to"] if reply_to_note.thread != validated_data["thread"]: - raise serializers.ValidationError( - "Reply to note should be in the same thread" - ) + msg = "Reply to note should be in the same thread" + raise serializers.ValidationError(msg) if reply_to_note.consultation != validated_data.get("consultation"): - raise serializers.ValidationError( - "Reply to note should be in the same consultation" - ) + msg = "Reply to note should be in the same consultation" + raise serializers.ValidationError(msg) user = self.context["request"].user note = validated_data.get("note") diff --git a/care/facility/api/serializers/patient_consultation.py b/care/facility/api/serializers/patient_consultation.py index 98c81d68df..25405b6f7e 100644 --- a/care/facility/api/serializers/patient_consultation.py +++ b/care/facility/api/serializers/patient_consultation.py @@ -66,8 +66,7 @@ from care.utils.lock import Lock from care.utils.notification_handler import NotificationGenerator from care.utils.queryset.facility import get_home_facility_queryset -from care.utils.serializer.external_id_field import ExternalIdSerializerField -from config.serializers import ChoiceField +from care.utils.serializers.fields import ChoiceField, ExternalIdSerializerField MIN_ENCOUNTER_DATE = make_aware(settings.MIN_ENCOUNTER_DATE) @@ -196,7 +195,8 @@ def _lock_key(self, patient_id): class Meta: model = PatientConsultation - read_only_fields = TIMESTAMP_FIELDS + ( + read_only_fields = ( + *TIMESTAMP_FIELDS, "last_updated_by_telemedicine", "discharge_date", "last_edited_by", @@ -227,10 +227,9 @@ def update(self, instance, validated_data): raise ValidationError( {"consultation": ["Discharged Consultation data cannot be updated"]} ) - else: - instance.medico_legal_case = validated_data.pop("medico_legal_case") - instance.save() - return instance + instance.medico_legal_case = validated_data.pop("medico_legal_case") + instance.save() + return instance if instance.suggestion == SuggestionChoices.OP: instance.discharge_date = localtime(now()) @@ -259,9 +258,12 @@ def update(self, instance, validated_data): self.context["request"].user == instance.assigned_to ) - if "is_kasp" in validated_data: - if validated_data["is_kasp"] and (not instance.is_kasp): - validated_data["kasp_enabled_date"] = localtime(now()) + if ( + "is_kasp" in validated_data + and validated_data["is_kasp"] + and (not instance.is_kasp) + ): + validated_data["kasp_enabled_date"] = localtime(now()) _temp = instance.assigned_to @@ -275,18 +277,21 @@ def update(self, instance, validated_data): old=old_instance, ) - if "assigned_to" in validated_data: - if validated_data["assigned_to"] != _temp and validated_data["assigned_to"]: - NotificationGenerator( - event=Notification.Event.PATIENT_CONSULTATION_ASSIGNMENT, - caused_by=self.context["request"].user, - caused_object=instance, - facility=instance.patient.facility, - notification_mediums=[ - Notification.Medium.SYSTEM, - Notification.Medium.WHATSAPP, - ], - ).generate() + if ( + "assigned_to" in validated_data + and validated_data["assigned_to"] != _temp + and validated_data["assigned_to"] + ): + NotificationGenerator( + event=Notification.Event.PATIENT_CONSULTATION_ASSIGNMENT, + caused_by=self.context["request"].user, + caused_object=instance, + facility=instance.patient.facility, + notification_mediums=[ + Notification.Medium.SYSTEM, + Notification.Medium.WHATSAPP, + ], + ).generate() NotificationGenerator( event=Notification.Event.PATIENT_CONSULTATION_UPDATED, @@ -297,7 +302,7 @@ def update(self, instance, validated_data): return consultation - def create(self, validated_data): + def create(self, validated_data): # noqa: PLR0915 PLR0912 if route_to_facility := validated_data.get("route_to_facility"): if route_to_facility == RouteToFacility.OUTPATIENT: validated_data["icu_admission_date"] = None @@ -387,9 +392,8 @@ def create(self, validated_data): {"consultation": "Exists please Edit Existing Consultation"} ) - if "is_kasp" in validated_data: - if validated_data["is_kasp"]: - validated_data["kasp_enabled_date"] = now() + if validated_data.get("is_kasp"): + validated_data["kasp_enabled_date"] = now() bed = validated_data.pop("bed", None) @@ -496,15 +500,18 @@ def create(self, validated_data): def validate_create_diagnoses(self, value): # Reject if create_diagnoses is present for edits if self.instance and value: - raise ValidationError("Bulk create diagnoses is not allowed on update") + msg = "Bulk create diagnoses is not allowed on update" + raise ValidationError(msg) # Reject if no diagnoses are provided if len(value) == 0: - raise ValidationError("Atleast one diagnosis is required") + msg = "Atleast one diagnosis is required" + raise ValidationError(msg) # Reject if duplicate diagnoses are provided - if len(value) != len(set([obj["diagnosis"].id for obj in value])): - raise ValidationError("Duplicate diagnoses are not allowed") + if len(value) != len({obj["diagnosis"].id for obj in value}): + msg = "Duplicate diagnoses are not allowed" + raise ValidationError(msg) principal_diagnosis, confirmed_diagnoses = None, [] for obj in value: @@ -514,9 +521,8 @@ def validate_create_diagnoses(self, value): # Reject if there are more than one principal diagnosis if obj["is_principal"]: if principal_diagnosis: - raise ValidationError( - "Only one diagnosis can be set as principal diagnosis" - ) + msg = "Only one diagnosis can be set as principal diagnosis" + raise ValidationError(msg) principal_diagnosis = obj # Reject if principal diagnosis is not one of confirmed diagnosis (if it is present) @@ -526,15 +532,15 @@ def validate_create_diagnoses(self, value): and principal_diagnosis["verification_status"] != ConditionVerificationStatus.CONFIRMED ): - raise ValidationError( - "Only confirmed diagnosis can be set as principal diagnosis if it is present" - ) + msg = "Only confirmed diagnosis can be set as principal diagnosis if it is present" + raise ValidationError(msg) return value def validate_create_symptoms(self, value): if self.instance: - raise ValidationError("Bulk create symptoms is not allowed on update") + msg = "Bulk create symptoms is not allowed on update" + raise ValidationError(msg) counter: set[int | str] = set() for obj in value: @@ -550,7 +556,8 @@ def validate_create_symptoms(self, value): item: str = other_symptom.strip().lower() if item in counter: # Reject if duplicate symptoms are provided - raise ValidationError("Duplicate symptoms are not allowed") + msg = "Duplicate symptoms are not allowed" + raise ValidationError(msg) if not obj.get("cure_date"): # skip duplicate symptom check for ones that has cure date counter.add(item) @@ -593,7 +600,7 @@ def validate_patient_no(self, value): return None return value.strip() - def validate(self, attrs): + def validate(self, attrs): # noqa: PLR0912 validated = super().validate(attrs) # TODO Add Bed Authorisation Validation @@ -621,8 +628,9 @@ def validate(self, attrs): ] } ) - if not treating_physician.user_type == User.TYPE_VALUE_MAP["Doctor"]: - raise ValidationError("Only Doctors can verify a Consultation") + if treating_physician.user_type != User.TYPE_VALUE_MAP["Doctor"]: + msg = "Only Doctors can verify a Consultation" + raise ValidationError(msg) facility = ( self.instance @@ -631,53 +639,48 @@ def validate(self, attrs): ) # Check if the Doctor is associated with the Facility (.facilities) if not treating_physician.facilities.filter(id=facility.id).exists(): - raise ValidationError( - "The treating doctor is no longer linked to this facility. Please update the respective field in the form before proceeding." - ) + msg = "The treating doctor is no longer linked to this facility. Please update the respective field in the form before proceeding." + raise ValidationError(msg) if ( treating_physician.home_facility and treating_physician.home_facility != facility + ): + msg = "Home Facility of the Doctor must be the same as the Consultation Facility" + raise ValidationError(msg) + + if "suggestion" in validated and validated["suggestion"] is SuggestionChoices.R: + if not validated.get("referred_to") and not validated.get( + "referred_to_external" ): raise ValidationError( - "Home Facility of the Doctor must be the same as the Consultation Facility" + { + "referred_to": [ + f"This field is required as the suggestion is {SuggestionChoices.R}." + ] + } ) + if validated.get("referred_to_external"): + validated["referred_to"] = None + elif validated.get("referred_to"): + validated["referred_to_external"] = None - if "suggestion" in validated: - if validated["suggestion"] is SuggestionChoices.R: - if not validated.get("referred_to") and not validated.get( - "referred_to_external" - ): - raise ValidationError( - { - "referred_to": [ - f"This field is required as the suggestion is {SuggestionChoices.R}." - ] - } - ) - if validated.get("referred_to_external"): - validated["referred_to"] = None - elif validated.get("referred_to"): - validated["referred_to_external"] = None - - if "action" in validated: - if validated["action"] == PatientRegistration.ActionEnum.REVIEW: - if "review_interval" not in validated: - raise ValidationError( - { - "review_interval": [ - "This field is required as the patient has been requested Review." - ] - } - ) - if validated["review_interval"] <= 0: - raise ValidationError( - { - "review_interval": [ - "This field value is must be greater than 0." - ] - } - ) + if ( + "action" in validated + and validated["action"] == PatientRegistration.ActionEnum.REVIEW + ): + if "review_interval" not in validated: + raise ValidationError( + { + "review_interval": [ + "This field is required as the patient has been requested Review." + ] + } + ) + if validated["review_interval"] <= 0: + raise ValidationError( + {"review_interval": ["This field value is must be greater than 0."]} + ) if not self.instance and "create_diagnoses" not in validated: raise ValidationError({"create_diagnoses": ["This field is required."]}) @@ -894,9 +897,8 @@ def get_files(self, obj): def validate_patient_code_status(self, value): if value == PatientCodeStatusType.NOT_SPECIFIED: - raise ValidationError( - "Specify a correct Patient Code Status for the Consent" - ) + msg = "Specify a correct Patient Code Status for the Consent" + raise ValidationError(msg) return value def validate(self, attrs): @@ -905,9 +907,8 @@ def validate(self, attrs): user.user_type < User.TYPE_VALUE_MAP["DistrictAdmin"] and self.context["consultation"].facility_id != user.home_facility_id ): - raise ValidationError( - "Only Home Facility Staff can create consent for a Consultation" - ) + msg = "Only Home Facility Staff can create consent for a Consultation" + raise ValidationError(msg) if ( attrs.get("type", None) @@ -936,9 +937,9 @@ def validate(self, attrs): ) return attrs - def clear_existing_records(self, consultation, type, user, self_id=None): + def clear_existing_records(self, consultation, _type, user, self_id=None): consents = PatientConsent.objects.filter( - consultation=consultation, type=type + consultation=consultation, type=_type ).exclude(id=self_id) archived_date = timezone.now() @@ -962,7 +963,7 @@ def create(self, validated_data): with transaction.atomic(): self.clear_existing_records( consultation=self.context["consultation"], - type=validated_data["type"], + _type=validated_data["type"], user=self.context["request"].user, ) validated_data["consultation"] = self.context["consultation"] @@ -973,7 +974,7 @@ def update(self, instance, validated_data): with transaction.atomic(): self.clear_existing_records( consultation=instance.consultation, - type=instance.type, + _type=instance.type, user=self.context["request"].user, self_id=instance.id, ) diff --git a/care/facility/api/serializers/patient_external_test.py b/care/facility/api/serializers/patient_external_test.py index 677c6b2e74..526dfd235e 100644 --- a/care/facility/api/serializers/patient_external_test.py +++ b/care/facility/api/serializers/patient_external_test.py @@ -23,13 +23,7 @@ class PatientExternalTestSerializer(serializers.ModelSerializer): ) result_date = serializers.DateField(input_formats=["%Y-%m-%d"], required=False) - def validate_empty_values(self, data, *args, **kwargs): - # if "is_repeat" in data: - # is_repeat = data["is_repeat"] - # if is_repeat.lower() == "yes": - # data["is_repeat"] = True - # else: - # data["is_repeat"] = False + def validate_empty_values(self, data, *args, **kwargs): # noqa: PLR0912 district_obj = None if "district" in data: district = data["district"] @@ -76,8 +70,10 @@ def validate_empty_values(self, data, *args, **kwargs): if "ward" in data and local_body_obj: try: int(data["ward"]) - except Exception: - raise ValidationError({"ward": ["Ward must be an integer value"]}) + except Exception as e: + raise ValidationError( + {"ward": ["Ward must be an integer value"]} + ) from e if data["ward"]: ward_obj = Ward.objects.filter( number=data["ward"], local_body=local_body_obj @@ -92,11 +88,13 @@ def validate_empty_values(self, data, *args, **kwargs): return super().validate_empty_values(data, *args, **kwargs) def create(self, validated_data): - if "srf_id" in validated_data: - if PatientRegistration.objects.filter( + if ( + "srf_id" in validated_data + and PatientRegistration.objects.filter( srf_id__iexact=validated_data["srf_id"] - ).exists(): - validated_data["patient_created"] = True + ).exists() + ): + validated_data["patient_created"] = True return super().create(validated_data) class Meta: diff --git a/care/facility/api/serializers/patient_icmr.py b/care/facility/api/serializers/patient_icmr.py index b90b5645dc..0e6ffb320d 100644 --- a/care/facility/api/serializers/patient_icmr.py +++ b/care/facility/api/serializers/patient_icmr.py @@ -7,7 +7,7 @@ PatientSampleICMR, ) from care.users.models import GENDER_CHOICES -from config.serializers import ChoiceField +from care.utils.serializers.fields import ChoiceField class ICMRPersonalDetails(serializers.ModelSerializer): diff --git a/care/facility/api/serializers/patient_investigation.py b/care/facility/api/serializers/patient_investigation.py index 65ab93e614..0c1ecc0664 100644 --- a/care/facility/api/serializers/patient_investigation.py +++ b/care/facility/api/serializers/patient_investigation.py @@ -15,7 +15,7 @@ class PatientInvestigationGroupSerializer(serializers.ModelSerializer): class Meta: model = PatientInvestigationGroup - exclude = TIMESTAMP_FIELDS + ("id",) + exclude = (*TIMESTAMP_FIELDS, "id") class PatientInvestigationSerializer(serializers.ModelSerializer): @@ -23,13 +23,13 @@ class PatientInvestigationSerializer(serializers.ModelSerializer): class Meta: model = PatientInvestigation - exclude = TIMESTAMP_FIELDS + ("id",) + exclude = (*TIMESTAMP_FIELDS, "id") class MinimalPatientInvestigationSerializer(serializers.ModelSerializer): class Meta: model = PatientInvestigation - exclude = TIMESTAMP_FIELDS + ("id", "groups") + exclude = (*TIMESTAMP_FIELDS, "id", "groups") class PatientInvestigationSessionSerializer(serializers.ModelSerializer): @@ -38,7 +38,7 @@ class PatientInvestigationSessionSerializer(serializers.ModelSerializer): class Meta: model = InvestigationSession - exclude = TIMESTAMP_FIELDS + ("external_id", "id") + exclude = (*TIMESTAMP_FIELDS, "external_id", "id") class InvestigationValueSerializer(serializers.ModelSerializer): @@ -58,13 +58,14 @@ class InvestigationValueSerializer(serializers.ModelSerializer): class Meta: model = InvestigationValue - read_only_fields = TIMESTAMP_FIELDS + ( + read_only_fields = ( + *TIMESTAMP_FIELDS, "session_id", "investigation", "consultation", "session", ) - exclude = TIMESTAMP_FIELDS + ("external_id",) + exclude = (*TIMESTAMP_FIELDS, "external_id") def update(self, instance, validated_data): if instance.consultation.discharge_date: @@ -72,14 +73,6 @@ def update(self, instance, validated_data): {"consultation": ["Discharged Consultation data cannot be updated"]} ) - # Removed since it might flood messages - # NotificationGenerator( - # event=Notification.Event.INVESTIGATION_UPDATED, - # caused_by=self.context["request"].user, - # caused_object=instance, - # facility=instance.consultation.patient.facility, - # ).generate() - return super().update(instance, validated_data) @@ -87,7 +80,7 @@ class InvestigationValueCreateSerializer(serializers.ModelSerializer): class Meta: model = InvestigationValue read_only_fields = TIMESTAMP_FIELDS - exclude = TIMESTAMP_FIELDS + ("external_id",) + exclude = (*TIMESTAMP_FIELDS, "external_id") class ValueSerializer(serializers.ModelSerializer): diff --git a/care/facility/api/serializers/patient_otp.py b/care/facility/api/serializers/patient_otp.py index 7457ac8454..6da20280df 100644 --- a/care/facility/api/serializers/patient_otp.py +++ b/care/facility/api/serializers/patient_otp.py @@ -1,4 +1,4 @@ -import random +import secrets import string from datetime import timedelta @@ -8,30 +8,16 @@ from rest_framework.exceptions import ValidationError from care.facility.models.patient import PatientMobileOTP -from care.utils.sms.sendSMS import sendSMS +from care.utils.sms.send_sms import send_sms def rand_pass(size): if not settings.USE_SMS: return "45612" - generate_pass = "".join( - [random.choice(string.ascii_uppercase + string.digits) for n in range(size)] - ) - - return generate_pass - -def send_sms(otp, phone_number): - if settings.USE_SMS: - sendSMS( - phone_number, - ( - f"Open Healthcare Network Patient Management System Login, OTP is {otp} . " - "Please do not share this Confidential Login Token with anyone else" - ), - ) - else: - print(otp, phone_number) + return "".join( + secrets.choice(string.ascii_uppercase + string.digits) for _ in range(size) + ) class PatientMobileOTPSerializer(serializers.ModelSerializer): @@ -56,7 +42,16 @@ def create(self, validated_data): otp_obj = super().create(validated_data) otp = rand_pass(settings.OTP_LENGTH) - send_sms(otp, otp_obj.phone_number) + if settings.USE_SMS: + send_sms( + otp_obj.phone_number, + ( + f"Open Healthcare Network Patient Management System Login, OTP is {otp} . " + "Please do not share this Confidential Login Token with anyone else" + ), + ) + elif settings.DEBUG: + print(otp, otp_obj.phone_number) # noqa: T201 otp_obj.otp = otp otp_obj.save() diff --git a/care/facility/api/serializers/patient_sample.py b/care/facility/api/serializers/patient_sample.py index 261d4f9ca3..e6e3a407c0 100644 --- a/care/facility/api/serializers/patient_sample.py +++ b/care/facility/api/serializers/patient_sample.py @@ -12,8 +12,7 @@ PatientSampleFlow, ) from care.users.api.serializers.user import UserBaseMinimumSerializer -from care.utils.serializer.external_id_field import ExternalIdSerializerField -from config.serializers import ChoiceField +from care.utils.serializers.fields import ChoiceField, ExternalIdSerializerField class PatientSampleFlowSerializer(serializers.ModelSerializer): @@ -79,7 +78,8 @@ class PatientSampleSerializer(serializers.ModelSerializer): class Meta: model = PatientSample - read_only_fields = TIMESTAMP_FIELDS + ( + read_only_fields = ( + *TIMESTAMP_FIELDS, "id", "facility", "last_edited_by", @@ -91,7 +91,7 @@ def create(self, validated_data): validated_data.pop("status", None) validated_data.pop("result", None) - sample = super(PatientSampleSerializer, self).create(validated_data) + sample = super().create(validated_data) sample.created_by = self.context["request"].user sample.last_edited_by = self.context["request"].user sample.save() @@ -119,8 +119,8 @@ def update(self, instance, validated_data): validated_data["status"] = PatientSample.SAMPLE_TEST_FLOW_MAP[ "COMPLETED" ] - except KeyError: - raise ValidationError({"status": ["is required"]}) + except KeyError as e: + raise ValidationError({"status": ["is required"]}) from e valid_choices = PatientSample.SAMPLE_FLOW_RULES[ PatientSample.SAMPLE_TEST_FLOW_CHOICES[instance.status - 1][1] ] @@ -134,7 +134,10 @@ def update(self, instance, validated_data): ) if choice == "COMPLETED" and not validated_data.get("result"): raise ValidationError({"result": ["is required as the test is complete"]}) - if choice == "COMPLETED" and instance.result != 3: + if ( + choice == "COMPLETED" + and instance.result != PatientSample.SAMPLE_TEST_RESULT_MAP["AWAITING"] + ): raise ValidationError( {"result": ["cannot change result for completed test."]} ) diff --git a/care/facility/api/serializers/prescription.py b/care/facility/api/serializers/prescription.py index f94d3cccff..71cd5c3544 100644 --- a/care/facility/api/serializers/prescription.py +++ b/care/facility/api/serializers/prescription.py @@ -32,13 +32,11 @@ class MedicineAdministrationSerializer(serializers.ModelSerializer): def validate_administered_date(self, value): if value > timezone.now(): - raise serializers.ValidationError( - "Administered Date cannot be in the future." - ) + msg = "Administered Date cannot be in the future." + raise serializers.ValidationError(msg) if self.context["prescription"].created_date > value: - raise serializers.ValidationError( - "Administered Date cannot be before Prescription Date." - ) + msg = "Administered Date cannot be before Prescription Date." + raise serializers.ValidationError(msg) return value def validate(self, attrs): @@ -50,9 +48,7 @@ def validate(self, attrs): raise serializers.ValidationError( {"dosage": "Dosage is required for titrated prescriptions."} ) - elif ( - self.context["prescription"].dosage_type != PrescriptionDosageType.TITRATED - ): + if self.context["prescription"].dosage_type != PrescriptionDosageType.TITRATED: attrs.pop("dosage", None) return super().validate(attrs) @@ -107,22 +103,24 @@ def validate(self, attrs): MedibaseMedicine, external_id=attrs["medicine"] ) - if not self.instance: - if Prescription.objects.filter( + if ( + not self.instance + and Prescription.objects.filter( consultation__external_id=self.context["request"].parser_context[ "kwargs" ]["consultation_external_id"], medicine=attrs["medicine"], discontinued=False, - ).exists(): - raise serializers.ValidationError( - { - "medicine": ( - "This medicine is already prescribed to this patient. " - "Please discontinue the existing prescription to prescribe again." - ) - } - ) + ).exists() + ): + raise serializers.ValidationError( + { + "medicine": ( + "This medicine is already prescribed to this patient. " + "Please discontinue the existing prescription to prescribe again." + ) + } + ) if not attrs.get("base_dosage"): raise serializers.ValidationError( diff --git a/care/facility/api/serializers/resources.py b/care/facility/api/serializers/resources.py index efc5df3294..3a6b920db6 100644 --- a/care/facility/api/serializers/resources.py +++ b/care/facility/api/serializers/resources.py @@ -13,8 +13,7 @@ ) from care.facility.models.resources import RESOURCE_SUB_CATEGORY_CHOICES from care.users.api.serializers.user import UserBaseMinimumSerializer -from care.utils.serializer.external_id_field import ExternalIdSerializerField -from config.serializers import ChoiceField +from care.utils.serializers.fields import ChoiceField, ExternalIdSerializerField def inverse_choices(choices): @@ -86,6 +85,7 @@ def __init__(self, instance=None, **kwargs): super().__init__(instance=instance, **kwargs) def update(self, instance, validated_data): + # ruff: noqa: N806 better to refactor this LIMITED_RECIEVING_STATUS_ = [] LIMITED_RECIEVING_STATUS = [ REVERSE_REQUEST_STATUS_CHOICES[x] for x in LIMITED_RECIEVING_STATUS_ @@ -101,18 +101,21 @@ def update(self, instance, validated_data): LIMITED_REQUEST_STATUS = [ REVERSE_REQUEST_STATUS_CHOICES[x] for x in LIMITED_REQUEST_STATUS_ ] - # LIMITED_ORGIN_STATUS = [] user = self.context["request"].user if "status" in validated_data: if validated_data["status"] in LIMITED_RECIEVING_STATUS: - if instance.assigned_facility: - if not has_facility_permission(user, instance.assigned_facility): - raise ValidationError({"status": ["Permission Denied"]}) - elif validated_data["status"] in LIMITED_REQUEST_STATUS: - if not has_facility_permission(user, instance.approving_facility): + if instance.assigned_facility and not has_facility_permission( + user, instance.assigned_facility + ): raise ValidationError({"status": ["Permission Denied"]}) + elif validated_data[ + "status" + ] in LIMITED_REQUEST_STATUS and not has_facility_permission( + user, instance.approving_facility + ): + raise ValidationError({"status": ["Permission Denied"]}) # Dont allow editing origin or patient if "origin_facility" in validated_data: @@ -120,9 +123,7 @@ def update(self, instance, validated_data): instance.last_edited_by = self.context["request"].user - new_instance = super().update(instance, validated_data) - - return new_instance + return super().update(instance, validated_data) def create(self, validated_data): # Do Validity checks for each of these data @@ -158,4 +159,4 @@ def create(self, validated_data): class Meta: model = ResourceRequestComment exclude = ("deleted", "request", "external_id") - read_only_fields = TIMESTAMP_FIELDS + ("created_by",) + read_only_fields = (*TIMESTAMP_FIELDS, "created_by") diff --git a/care/facility/api/serializers/shifting.py b/care/facility/api/serializers/shifting.py index 6c55f56d7a..9d6c91f688 100644 --- a/care/facility/api/serializers/shifting.py +++ b/care/facility/api/serializers/shifting.py @@ -33,8 +33,7 @@ from care.users.api.serializers.lsg import StateSerializer from care.users.api.serializers.user import UserBaseMinimumSerializer from care.utils.notification_handler import NotificationGenerator -from care.utils.serializer.external_id_field import ExternalIdSerializerField -from config.serializers import ChoiceField +from care.utils.serializers.fields import ChoiceField, ExternalIdSerializerField def inverse_choices(choices): @@ -44,7 +43,9 @@ def inverse_choices(choices): return output -REVERSE_SHIFTING_STATUS_CHOICES = inverse_choices(SHIFTING_STATUS_CHOICES) +REVERSE_SHIFTING_STATUS_CHOICES: dict[str, int] = inverse_choices( + SHIFTING_STATUS_CHOICES +) def has_facility_permission(user, facility): @@ -240,14 +241,17 @@ def __init__(self, instance=None, **kwargs): def validate_shifting_approving_facility(self, value): if not settings.PEACETIME_MODE and not value: - raise ValidationError("Shifting Approving Facility is required") + msg = "Shifting Approving Facility is required" + raise ValidationError(msg) return value - def update(self, instance, validated_data): + def update(self, instance, validated_data): # noqa: PLR0912 if instance.status == REVERSE_SHIFTING_STATUS_CHOICES["CANCELLED"]: - raise ValidationError("Permission Denied, Shifting request was cancelled.") - elif instance.status == REVERSE_SHIFTING_STATUS_CHOICES["COMPLETED"]: - raise ValidationError("Permission Denied, Shifting request was completed.") + msg = "Permission Denied, Shifting request was cancelled." + raise ValidationError(msg) + if instance.status == REVERSE_SHIFTING_STATUS_CHOICES["COMPLETED"]: + msg = "Permission Denied, Shifting request was completed." + raise ValidationError(msg) # Dont allow editing origin or patient validated_data.pop("origin_facility") @@ -289,11 +293,15 @@ def update(self, instance, validated_data): raise ValidationError({"status": ["Permission Denied"]}) elif ( - status in self.LIMITED_RECIEVING_STATUS - and instance.assigned_facility - and not has_facility_permission(user, instance.assigned_facility) - ) or status in self.LIMITED_SHIFTING_STATUS and not has_facility_permission( - user, instance.shifting_approving_facility + ( + status in self.LIMITED_RECIEVING_STATUS + and instance.assigned_facility + and not has_facility_permission(user, instance.assigned_facility) + ) + or status in self.LIMITED_SHIFTING_STATUS + and not has_facility_permission( + user, instance.shifting_approving_facility + ) ): raise ValidationError({"status": ["Permission Denied"]}) @@ -346,7 +354,8 @@ def update(self, instance, validated_data): "status" in validated_data and new_instance.shifting_approving_facility is not None and validated_data["status"] != old_status - and validated_data["status"] == 40 + and validated_data["status"] + == REVERSE_SHIFTING_STATUS_CHOICES["DESTINATION APPROVED"] ): NotificationGenerator( event=Notification.Event.SHIFTING_UPDATED, @@ -569,8 +578,4 @@ def create(self, validated_data): class Meta: model = ShiftingRequestComment exclude = ("deleted", "request") - read_only_fields = TIMESTAMP_FIELDS + ( - "created_by", - "external_id", - "id", - ) + read_only_fields = (*TIMESTAMP_FIELDS, "created_by", "external_id", "id") diff --git a/care/facility/api/viewsets/ambulance.py b/care/facility/api/viewsets/ambulance.py index a2dbe3de95..49387d2bce 100644 --- a/care/facility/api/viewsets/ambulance.py +++ b/care/facility/api/viewsets/ambulance.py @@ -58,7 +58,7 @@ class AmbulanceViewSet( def get_serializer_class(self): if self.action == "add_driver": return AmbulanceDriverSerializer - elif self.action == "remove_driver": + if self.action == "remove_driver": return DeleteDriverSerializer return AmbulanceSerializer diff --git a/care/facility/api/viewsets/asset.py b/care/facility/api/viewsets/asset.py index 26b9d35b34..15dd00e2aa 100644 --- a/care/facility/api/viewsets/asset.py +++ b/care/facility/api/viewsets/asset.py @@ -1,4 +1,6 @@ +import logging import re +from typing import TYPE_CHECKING from django.conf import settings from django.core.cache import cache @@ -58,13 +60,18 @@ ) from care.users.models import User from care.utils.assetintegration.asset_classes import AssetClasses -from care.utils.assetintegration.base import BaseAssetIntegration from care.utils.cache.cache_allowed_facilities import get_accessible_facilities from care.utils.filters.choicefilter import CareChoiceFilter, inverse_choices from care.utils.queryset.asset_location import get_asset_location_queryset from care.utils.queryset.facility import get_facility_queryset from config.authentication import MiddlewareAuthentication +if TYPE_CHECKING: + from care.utils.assetintegration.base import BaseAssetIntegration + +logger = logging.getLogger(__name__) + + inverse_asset_type = inverse_choices(AssetTypeChoices) inverse_asset_status = inverse_choices(StatusChoices) @@ -131,9 +138,11 @@ def perform_create(self, serializer): def destroy(self, request, *args, **kwargs): instance = self.get_object() if instance.bed_set.filter(deleted=False).count(): - raise ValidationError("Cannot delete a Location with associated Beds") + msg = "Cannot delete a Location with associated Beds" + raise ValidationError(msg) if instance.asset_set.filter(deleted=False).count(): - raise ValidationError("Cannot delete a Location with associated Assets") + msg = "Cannot delete a Location with associated Assets" + raise ValidationError(msg) return super().destroy(request, *args, **kwargs) @@ -243,11 +252,9 @@ def get_queryset(self): content_type__model="asset", object_external_id=self.kwargs["asset_external_id"], ) - else: - raise exceptions.PermissionDenied( - "You do not have access to this asset's availability records" - ) - elif "asset_location_external_id" in self.kwargs: + msg = "You do not have access to this asset's availability records" + raise exceptions.PermissionDenied(msg) + if "asset_location_external_id" in self.kwargs: asset_location = get_object_or_404( AssetLocation, external_id=self.kwargs["asset_location_external_id"] ) @@ -256,14 +263,10 @@ def get_queryset(self): content_type__model="assetlocation", object_external_id=self.kwargs["asset_location_external_id"], ) - else: - raise exceptions.PermissionDenied( - "You do not have access to this asset location's availability records" - ) - else: - raise exceptions.ValidationError( - "Either asset_external_id or asset_location_external_id is required" - ) + msg = "You do not have access to this asset location's availability records" + raise exceptions.PermissionDenied(msg) + msg = "Either asset_external_id or asset_location_external_id is required" + raise exceptions.ValidationError(msg) class AssetViewSet( @@ -302,7 +305,7 @@ def get_queryset(self): queryset = queryset.filter( current_location__facility__id__in=allowed_facilities ) - queryset = queryset.annotate( + return queryset.annotate( latest_status=Subquery( AvailabilityRecord.objects.filter( content_type__model="asset", @@ -312,7 +315,6 @@ def get_queryset(self): .values("status")[:1] ) ) - return queryset def list(self, request, *args, **kwargs): if settings.CSV_REQUEST_PARAMETER in request.GET: @@ -323,16 +325,14 @@ def list(self, request, *args, **kwargs): queryset, field_header_map=mapping, field_serializer_map=pretty_mapping ) - return super(AssetViewSet, self).list(request, *args, **kwargs) + return super().list(request, *args, **kwargs) def destroy(self, request, *args, **kwargs): user = self.request.user if user.user_type >= User.TYPE_VALUE_MAP["DistrictAdmin"]: return super().destroy(request, *args, **kwargs) - else: - raise exceptions.AuthenticationFailed( - "Only District Admin and above can delete assets" - ) + msg = "Only District Admin and above can delete assets" + raise exceptions.AuthenticationFailed(msg) @extend_schema( responses={200: UserDefaultAssetLocationSerializer()}, tags=["asset"] @@ -416,7 +416,7 @@ def operate_assets(self, request, *args, **kwargs): ) except Exception as e: - print(f"error: {e}") + logger.info("Failed to operate asset: %s", e) return Response( {"message": "Internal Server Error"}, status=status.HTTP_500_INTERNAL_SERVER_ERROR, diff --git a/care/facility/api/viewsets/bed.py b/care/facility/api/viewsets/bed.py index 2f994f108d..336b5f83c2 100644 --- a/care/facility/api/viewsets/bed.py +++ b/care/facility/api/viewsets/bed.py @@ -131,7 +131,7 @@ def create(self, request, *args, **kwargs): def destroy(self, request, *args, **kwargs): if request.user.user_type < User.TYPE_VALUE_MAP["DistrictLabAdmin"]: - raise PermissionDenied() + raise PermissionDenied instance = self.get_object() if instance.is_occupied: raise DRFValidationError( diff --git a/care/facility/api/viewsets/daily_round.py b/care/facility/api/viewsets/daily_round.py index 612c451437..9786ffb312 100644 --- a/care/facility/api/viewsets/daily_round.py +++ b/care/facility/api/viewsets/daily_round.py @@ -14,7 +14,7 @@ from care.facility.models.daily_round import DailyRound from care.utils.queryset.consultation import get_consultation_queryset -DailyRoundAttributes = [f.name for f in DailyRound._meta.get_fields()] +DailyRoundAttributes = [f.name for f in DailyRound._meta.get_fields()] # noqa: SLF001 class DailyRoundFilterSet(filters.FilterSet): @@ -98,9 +98,6 @@ def analyse(self, request, **kwargs): page = request.data.get("page", 1) - # to_time = datetime.now() - timedelta(days=((page - 1) * self.DEFAULT_LOOKUP_DAYS)) - # from_time = to_time - timedelta(days=self.DEFAULT_LOOKUP_DAYS) - consultation = get_object_or_404( get_consultation_queryset(request.user).filter( external_id=self.kwargs["consultation_external_id"] diff --git a/care/facility/api/viewsets/events.py b/care/facility/api/viewsets/events.py index 8dc1678b4d..b1fedf92e4 100644 --- a/care/facility/api/viewsets/events.py +++ b/care/facility/api/viewsets/events.py @@ -68,8 +68,6 @@ class PatientConsultationEventViewSet(ReadOnlyModelViewSet): ) filter_backends = (filters.DjangoFilterBackend,) filterset_class = PatientConsultationEventFilterSet - # lookup_field = "external_id" - # lookup_url_kwarg = "external_id" def get_consultation_obj(self): return get_object_or_404( diff --git a/care/facility/api/viewsets/facility.py b/care/facility/api/viewsets/facility.py index 222f0591fd..dd8221c0cb 100644 --- a/care/facility/api/viewsets/facility.py +++ b/care/facility/api/viewsets/facility.py @@ -144,7 +144,7 @@ def list(self, request, *args, **kwargs): queryset, field_header_map=mapping, field_serializer_map=pretty_mapping ) - return super(FacilityViewSet, self).list(request, *args, **kwargs) + return super().list(request, *args, **kwargs) @extend_schema(tags=["facility"]) @method_decorator(parser_classes([MultiPartParser])) diff --git a/care/facility/api/viewsets/facility_capacity.py b/care/facility/api/viewsets/facility_capacity.py index dd18d13f62..6bc28fce13 100644 --- a/care/facility/api/viewsets/facility_capacity.py +++ b/care/facility/api/viewsets/facility_capacity.py @@ -27,9 +27,9 @@ def get_queryset(self): ) if user.is_superuser: return queryset - elif self.request.user.user_type >= User.TYPE_VALUE_MAP["StateLabAdmin"]: + if self.request.user.user_type >= User.TYPE_VALUE_MAP["StateLabAdmin"]: return queryset.filter(facility__state=user.state) - elif self.request.user.user_type >= User.TYPE_VALUE_MAP["DistrictLabAdmin"]: + if self.request.user.user_type >= User.TYPE_VALUE_MAP["DistrictLabAdmin"]: return queryset.filter(facility__district=user.district) return queryset.filter(facility__users__id__exact=user.id) diff --git a/care/facility/api/viewsets/facility_users.py b/care/facility/api/viewsets/facility_users.py index 5d3cb79629..fb2cf25916 100644 --- a/care/facility/api/viewsets/facility_users.py +++ b/care/facility/api/viewsets/facility_users.py @@ -47,5 +47,5 @@ def get_queryset(self): queryset=UserSkill.objects.filter(skill__deleted=False), ), ) - except Facility.DoesNotExist: - raise ValidationError({"Facility": "Facility not found"}) + except Facility.DoesNotExist as e: + raise ValidationError({"Facility": "Facility not found"}) from e diff --git a/care/facility/api/viewsets/file_upload.py b/care/facility/api/viewsets/file_upload.py index 5e784fafb6..bcf98282f0 100644 --- a/care/facility/api/viewsets/file_upload.py +++ b/care/facility/api/viewsets/file_upload.py @@ -37,11 +37,10 @@ def has_permission(self, request, view) -> bool: "PATIENT", "CONSULTATION", ) - else: - return request.data.get("file_type") not in ( - "PATIENT", - "CONSULTATION", - ) + return request.data.get("file_type") not in ( + "PATIENT", + "CONSULTATION", + ) return True def has_object_permission(self, request, view, obj) -> bool: @@ -67,12 +66,11 @@ class FileUploadViewSet( def get_serializer_class(self): if self.action == "retrieve": return FileUploadRetrieveSerializer - elif self.action == "list": + if self.action == "list": return FileUploadListSerializer - elif self.action == "create": + if self.action == "create": return FileUploadCreateSerializer - else: - return FileUploadUpdateSerializer + return FileUploadUpdateSerializer def get_queryset(self): if "file_type" not in self.request.GET: diff --git a/care/facility/api/viewsets/icd.py b/care/facility/api/viewsets/icd.py index e18d86de23..064707ec71 100644 --- a/care/facility/api/viewsets/icd.py +++ b/care/facility/api/viewsets/icd.py @@ -8,7 +8,6 @@ class ICDViewSet(ViewSet): - def serialize_data(self, objects: list[ICD11]): return [diagnosis.get_representation() for diagnosis in objects] diff --git a/care/facility/api/viewsets/inventory.py b/care/facility/api/viewsets/inventory.py index e50d353635..20c87650e8 100644 --- a/care/facility/api/viewsets/inventory.py +++ b/care/facility/api/viewsets/inventory.py @@ -33,7 +33,17 @@ ) from care.users.models import User from care.utils.queryset.facility import get_facility_queryset -from care.utils.validation.integer_validation import check_integer + + +def check_integer(vals): + if not isinstance(vals, list): + vals = [vals] + for i in range(len(vals)): + try: + vals[i] = int(vals[i]) + except Exception as e: + raise ValidationError({"value": "Integer Required"}) from e + return vals class FacilityInventoryFilter(filters.FilterSet): @@ -87,9 +97,9 @@ def get_queryset(self): ) if user.is_superuser: return queryset - elif self.request.user.user_type >= User.TYPE_VALUE_MAP["StateLabAdmin"]: + if self.request.user.user_type >= User.TYPE_VALUE_MAP["StateLabAdmin"]: return queryset.filter(facility__state=user.state) - elif self.request.user.user_type >= User.TYPE_VALUE_MAP["DistrictLabAdmin"]: + if self.request.user.user_type >= User.TYPE_VALUE_MAP["DistrictLabAdmin"]: return queryset.filter(facility__district=user.district) return queryset.filter(facility__users__id__exact=user.id) @@ -171,9 +181,9 @@ def get_queryset(self): ) if user.is_superuser: return queryset - elif self.request.user.user_type >= User.TYPE_VALUE_MAP["StateLabAdmin"]: + if self.request.user.user_type >= User.TYPE_VALUE_MAP["StateLabAdmin"]: return queryset.filter(facility__state=user.state) - elif self.request.user.user_type >= User.TYPE_VALUE_MAP["DistrictLabAdmin"]: + if self.request.user.user_type >= User.TYPE_VALUE_MAP["DistrictLabAdmin"]: return queryset.filter(facility__district=user.district) return queryset.filter(facility__users__id__exact=user.id) @@ -215,9 +225,9 @@ def get_queryset(self): ) if user.is_superuser: return queryset - elif self.request.user.user_type >= User.TYPE_VALUE_MAP["StateLabAdmin"]: + if self.request.user.user_type >= User.TYPE_VALUE_MAP["StateLabAdmin"]: return queryset.filter(facility__state=user.state) - elif self.request.user.user_type >= User.TYPE_VALUE_MAP["DistrictLabAdmin"]: + if self.request.user.user_type >= User.TYPE_VALUE_MAP["DistrictLabAdmin"]: return queryset.filter(facility__district=user.district) return queryset.filter(facility__users__id__exact=user.id) @@ -225,26 +235,3 @@ def get_object(self): return get_object_or_404( self.get_queryset(), external_id=self.kwargs.get("external_id") ) - - -# class FacilityInventoryBurnRateFilter(filters.FilterSet): -# name = filters.CharFilter(field_name="facility__name", lookup_expr="icontains") -# item = filters.NumberFilter(field_name="item_id") - - -# class FacilityInventoryBurnRateViewSet( -# UserAccessMixin, ListModelMixin, RetrieveModelMixin, GenericViewSet, -# ): -# queryset = FacilityInventoryBurnRate.objects.select_related( -# "item", "item__default_unit", "facility__district" -# ).all() -# filter_backends = (filters.DjangoFilterBackend,) -# filterset_class = FacilityInventoryBurnRateFilter -# permission_classes = (IsAuthenticated, DRYPermissions) -# serializer_class = FacilityInventoryBurnRateSerializer - -# def filter_queryset(self, queryset): -# queryset = super().filter_queryset(queryset) -# if self.kwargs.get("facility_external_id"): -# queryset = queryset.filter(facility__external_id=self.kwargs.get("facility_external_id")) -# return self.filter_by_user_scope(queryset) diff --git a/care/facility/api/viewsets/mixins/access.py b/care/facility/api/viewsets/mixins/access.py index 09f25f6d18..314c36c360 100644 --- a/care/facility/api/viewsets/mixins/access.py +++ b/care/facility/api/viewsets/mixins/access.py @@ -53,7 +53,7 @@ class AssetUserAccessMixin: asset_permissions = (DRYAssetPermissions,) def get_authenticators(self): - return [MiddlewareAssetAuthentication()] + super().get_authenticators() + return [MiddlewareAssetAuthentication(), *super().get_authenticators()] def get_permissions(self): """ diff --git a/care/facility/api/viewsets/mixins/history.py b/care/facility/api/viewsets/mixins/history.py index 2727e3f125..77143a3551 100644 --- a/care/facility/api/viewsets/mixins/history.py +++ b/care/facility/api/viewsets/mixins/history.py @@ -1,6 +1,6 @@ from rest_framework.decorators import action -from care.utils.serializer.history_serializer import ModelHistorySerializer +from care.utils.serializers.history_serializer import ModelHistorySerializer class HistoryMixin: diff --git a/care/facility/api/viewsets/notification.py b/care/facility/api/viewsets/notification.py index 213f1324e9..a51097f8e2 100644 --- a/care/facility/api/viewsets/notification.py +++ b/care/facility/api/viewsets/notification.py @@ -68,7 +68,7 @@ def public_key(self, request, *args, **kwargs): def notify(self, request, *args, **kwargs): user = request.user if user.user_type < User.TYPE_VALUE_MAP["Doctor"]: - raise PermissionDenied() + raise PermissionDenied if "facility" not in request.data or request.data["facility"] == "": raise ValidationError({"facility": "is required"}) if "message" not in request.data or request.data["message"] == "": diff --git a/care/facility/api/viewsets/patient.py b/care/facility/api/viewsets/patient.py index 043c1046ab..72731cd6e2 100644 --- a/care/facility/api/viewsets/patient.py +++ b/care/facility/api/viewsets/patient.py @@ -1,4 +1,3 @@ -import datetime import json from json import JSONDecodeError @@ -97,7 +96,6 @@ class PatientFilterSet(filters.FilterSet): - last_consultation_field = "last_consultation" source = filters.ChoiceFilter(choices=PatientRegistration.SourceChoices) @@ -356,7 +354,7 @@ class PatientCustomOrderingFilter(BaseFilterBackend): def filter_queryset(self, request, queryset, view): ordering = request.query_params.get("ordering", "") - if ordering == "category_severity" or ordering == "-category_severity": + if ordering in ("category_severity", "-category_severity"): category_ordering = { category: index + 1 for index, (category, _) in enumerate(CATEGORY_CHOICES) @@ -507,12 +505,11 @@ def get_queryset(self): def get_serializer_class(self): if self.action == "list": return PatientListSerializer - elif self.action == "icmr_sample": + if self.action == "icmr_sample": return PatientICMRSerializer - elif self.action == "transfer": + if self.action == "transfer": return PatientTransferSerializer - else: - return self.serializer_class + return self.serializer_class def filter_queryset(self, queryset: QuerySet) -> QuerySet: if self.action == "list" and settings.CSV_REQUEST_PARAMETER in self.request.GET: @@ -586,7 +583,7 @@ def list(self, request, *args, **kwargs): field_serializer_map=PatientRegistration.CSV_MAKE_PRETTY, ) - return super(PatientViewSet, self).list(request, *args, **kwargs) + return super().list(request, *args, **kwargs) @extend_schema(tags=["patient"]) @action(detail=True, methods=["POST"]) @@ -805,9 +802,9 @@ def get_queryset(self): ) if user.is_superuser: return queryset - elif self.request.user.user_type >= User.TYPE_VALUE_MAP["StateLabAdmin"]: + if self.request.user.user_type >= User.TYPE_VALUE_MAP["StateLabAdmin"]: return queryset.filter(facility__state=user.state) - elif self.request.user.user_type >= User.TYPE_VALUE_MAP["DistrictLabAdmin"]: + if self.request.user.user_type >= User.TYPE_VALUE_MAP["DistrictLabAdmin"]: return queryset.filter(facility__district=user.district) return queryset.filter(facility__users__id__exact=user.id) @@ -836,9 +833,7 @@ def list(self, request, *args, **kwargs): - entry_date_before: date in YYYY-MM-DD format, inclusive of this date """ - return super(FacilityPatientStatsHistoryViewSet, self).list( - request, *args, **kwargs - ) + return super().list(request, *args, **kwargs) class PatientSearchSetPagination(PageNumberPagination): @@ -864,62 +859,58 @@ class PatientSearchViewSet(ListModelMixin, GenericViewSet): def get_queryset(self): if self.action != "list": - return super(PatientSearchViewSet, self).get_queryset() + return super().get_queryset() + serializer = PatientSearchSerializer( + data=self.request.query_params, partial=True + ) + serializer.is_valid(raise_exception=True) + if self.request.user.user_type >= User.TYPE_VALUE_MAP["DistrictLabAdmin"]: + search_keys = [ + "date_of_birth", + "year_of_birth", + "phone_number", + "name", + "age", + ] else: - serializer = PatientSearchSerializer( - data=self.request.query_params, partial=True + search_keys = [ + "date_of_birth", + "year_of_birth", + "phone_number", + "age", + ] + search_fields = { + key: serializer.validated_data[key] + for key in search_keys + if serializer.validated_data.get(key) + } + if not search_fields: + raise serializers.ValidationError( + { + "detail": [ + f"None of the search keys provided. Available: {', '.join(search_keys)}" + ] + } ) - serializer.is_valid(raise_exception=True) - if self.request.user.user_type >= User.TYPE_VALUE_MAP["DistrictLabAdmin"]: - search_keys = [ - "date_of_birth", - "year_of_birth", - "phone_number", - "name", - "age", - ] - else: - search_keys = [ - "date_of_birth", - "year_of_birth", - "phone_number", - "age", - ] - search_fields = { - key: serializer.validated_data[key] - for key in search_keys - if serializer.validated_data.get(key) - } - if not search_fields: - raise serializers.ValidationError( - { - "detail": [ - f"None of the search keys provided. Available: {', '.join(search_keys)}" - ] - } - ) - - # if not self.request.user.is_superuser: - # search_fields["state_id"] = self.request.user.state_id - if "age" in search_fields: - age = search_fields.pop("age") - year_of_birth = datetime.datetime.now().year - age - search_fields["age__gte"] = year_of_birth - 5 - search_fields["age__lte"] = year_of_birth + 5 + if "age" in search_fields: + age = search_fields.pop("age") + year_of_birth = timezone.now().year - age + search_fields["age__gte"] = year_of_birth - 5 + search_fields["age__lte"] = year_of_birth + 5 - name = search_fields.pop("name", None) + name = search_fields.pop("name", None) - queryset = self.queryset.filter(**search_fields) + queryset = self.queryset.filter(**search_fields) - if name: - queryset = ( - queryset.annotate(similarity=TrigramSimilarity("name", name)) - .filter(similarity__gt=0.2) - .order_by("-similarity") - ) + if name: + queryset = ( + queryset.annotate(similarity=TrigramSimilarity("name", name)) + .filter(similarity__gt=0.2) + .order_by("-similarity") + ) - return queryset + return queryset @extend_schema(tags=["patient"]) def list(self, request, *args, **kwargs): @@ -939,7 +930,7 @@ def list(self, request, *args, **kwargs): `Eg: api/v1/patient/search/?year_of_birth=1992&phone_number=%2B917795937091` """ - return super(PatientSearchViewSet, self).list(request, *args, **kwargs) + return super().list(request, *args, **kwargs) class PatientNotesFilterSet(filters.FilterSet): diff --git a/care/facility/api/viewsets/patient_consultation.py b/care/facility/api/viewsets/patient_consultation.py index b166971dc9..8bd7145df9 100644 --- a/care/facility/api/viewsets/patient_consultation.py +++ b/care/facility/api/viewsets/patient_consultation.py @@ -70,12 +70,11 @@ class PatientConsultationViewSet( def get_serializer_class(self): if self.action == "patient_from_asset": return PatientConsultationIDSerializer - elif self.action == "discharge_patient": + if self.action == "discharge_patient": return PatientConsultationDischargeSerializer - elif self.action == "email_discharge_summary": + if self.action == "email_discharge_summary": return EmailDischargeSummarySerializer - else: - return self.serializer_class + return self.serializer_class def get_permissions(self): if self.action == "patient_from_asset": @@ -97,11 +96,11 @@ def get_queryset(self): ) if self.request.user.is_superuser: return self.queryset - elif self.request.user.user_type >= User.TYPE_VALUE_MAP["StateLabAdmin"]: + if self.request.user.user_type >= User.TYPE_VALUE_MAP["StateLabAdmin"]: return self.queryset.filter( patient__facility__state=self.request.user.state ) - elif self.request.user.user_type >= User.TYPE_VALUE_MAP["DistrictLabAdmin"]: + if self.request.user.user_type >= User.TYPE_VALUE_MAP["DistrictLabAdmin"]: return self.queryset.filter( patient__facility__district=self.request.user.district ) @@ -304,14 +303,12 @@ def dev_preview_discharge_summary(request, consultation_id): with tempfile.NamedTemporaryFile(delete=False, suffix=".pdf") as tmp_file: discharge_summary.generate_discharge_summary_pdf(data, tmp_file) + tmp_file.seek(0) - with open(tmp_file.name, "rb") as pdf_file: - pdf_content = pdf_file.read() - - response = HttpResponse(pdf_content, content_type="application/pdf") - response["Content-Disposition"] = 'inline; filename="discharge_summary.pdf"' + response = HttpResponse(tmp_file, content_type="application/pdf") + response["Content-Disposition"] = 'inline; filename="discharge_summary.pdf"' - return response + return response class PatientConsentViewSet( diff --git a/care/facility/api/viewsets/patient_external_test.py b/care/facility/api/viewsets/patient_external_test.py index f5d983a9ed..e1d86cc311 100644 --- a/care/facility/api/viewsets/patient_external_test.py +++ b/care/facility/api/viewsets/patient_external_test.py @@ -28,9 +28,9 @@ from care.users.models import User -def prettyerrors(errors): +def pretty_errors(errors): pretty_errors = defaultdict(list) - for attribute in PatientExternalTest.HEADER_CSV_MAPPING.keys(): + for attribute in PatientExternalTest.HEADER_CSV_MAPPING: if attribute in errors: for error in errors.get(attribute, ""): pretty_errors[attribute].append(str(error)) @@ -46,8 +46,7 @@ def filter(self, qs, value): self.field_name + "__in": values, self.field_name + "__isnull": False, } - qs = qs.filter(**_filter) - return qs + return qs.filter(**_filter) class PatientExternalTestFilter(filters.FilterSet): @@ -113,22 +112,20 @@ def get_queryset(self): return queryset def get_serializer_class(self): - if self.action == "update" or self.action == "partial_update": + if self.action in ("update", "partial_update"): return PatientExternalTestUpdateSerializer return super().get_serializer_class() def destroy(self, request, *args, **kwargs): if self.request.user.user_type < User.TYPE_VALUE_MAP["DistrictLabAdmin"]: - raise PermissionDenied() + raise PermissionDenied return super().destroy(request, *args, **kwargs) def check_upload_permission(self): - if ( + return bool( self.request.user.is_superuser is True or self.request.user.user_type >= User.TYPE_VALUE_MAP["DistrictLabAdmin"] - ): - return True - return False + ) def list(self, request, *args, **kwargs): if settings.CSV_REQUEST_PARAMETER in request.GET: @@ -140,18 +137,14 @@ def list(self, request, *args, **kwargs): field_header_map=mapping, field_serializer_map=pretty_mapping, ) - return super(PatientExternalTestViewSet, self).list(request, *args, **kwargs) + return super().list(request, *args, **kwargs) @extend_schema(tags=["external_result"]) @action(methods=["POST"], detail=False) def bulk_upsert(self, request, *args, **kwargs): if not self.check_upload_permission(): - raise PermissionDenied("Permission to Endpoint Denied") - # if len(request.FILES.keys()) != 1: - # raise ValidationError({"file": "Upload 1 File at a time"}) - # csv_file = request.FILES[list(request.FILES.keys())[0]] - # csv_file.seek(0) - # reader = csv.DictReader(io.StringIO(csv_file.read().decode("utf-8-sig"))) + msg = "Permission to Endpoint Denied" + raise PermissionDenied(msg) if "sample_tests" not in request.data: raise ValidationError({"sample_tests": "No Data was provided"}) if not isinstance(request.data["sample_tests"], list): @@ -163,18 +156,16 @@ def bulk_upsert(self, request, *args, **kwargs): raise ValidationError({"Error": "User must belong to same district"}) errors = [] - counter = 0 ser_objects = [] invalid = False for sample in request.data["sample_tests"]: - counter += 1 - serialiser_obj = PatientExternalTestSerializer(data=sample) - valid = serialiser_obj.is_valid() - current_error = prettyerrors(serialiser_obj._errors) + serializer = PatientExternalTestSerializer(data=sample) + valid = serializer.is_valid() + current_error = pretty_errors(serializer._errors) # noqa: SLF001 if current_error and (not valid): errors.append(current_error) invalid = True - ser_objects.append(serialiser_obj) + ser_objects.append(serializer) if invalid: return Response(errors, status=status.HTTP_400_BAD_REQUEST) for ser_object in ser_objects: diff --git a/care/facility/api/viewsets/patient_investigation.py b/care/facility/api/viewsets/patient_investigation.py index 07350e4a41..8a572de500 100644 --- a/care/facility/api/viewsets/patient_investigation.py +++ b/care/facility/api/viewsets/patient_investigation.py @@ -44,8 +44,7 @@ def filter(self, qs, value): if not value: return qs - qs = qs.filter(groups__external_id=value) - return qs + return qs.filter(groups__external_id=value) class PatientInvestigationFilter(filters.FilterSet): @@ -115,8 +114,7 @@ def get_queryset(self): queryset.filter(investigation__external_id__in=investigations.split(",")) .order_by("-session__created_date") .distinct("session__created_date")[ - (session_page - 1) - * self.SESSION_PER_PAGE : (session_page) + (session_page - 1) * self.SESSION_PER_PAGE : (session_page) * self.SESSION_PER_PAGE ] ) @@ -125,11 +123,11 @@ def get_queryset(self): queryset = queryset.filter(session_id__in=sessions.values("session_id")) if self.request.user.is_superuser: return queryset - elif self.request.user.user_type >= User.TYPE_VALUE_MAP["StateLabAdmin"]: + if self.request.user.user_type >= User.TYPE_VALUE_MAP["StateLabAdmin"]: return queryset.filter( consultation__patient__facility__state=self.request.user.state ) - elif self.request.user.user_type >= User.TYPE_VALUE_MAP["DistrictLabAdmin"]: + if self.request.user.user_type >= User.TYPE_VALUE_MAP["DistrictLabAdmin"]: return queryset.filter( consultation__patient__facility__district=self.request.user.district ) @@ -168,11 +166,11 @@ def get_queryset(self): ) if self.request.user.is_superuser: return queryset - elif self.request.user.user_type >= User.TYPE_VALUE_MAP["StateLabAdmin"]: + if self.request.user.user_type >= User.TYPE_VALUE_MAP["StateLabAdmin"]: return queryset.filter( consultation__patient__facility__state=self.request.user.state ) - elif self.request.user.user_type >= User.TYPE_VALUE_MAP["DistrictLabAdmin"]: + if self.request.user.user_type >= User.TYPE_VALUE_MAP["DistrictLabAdmin"]: return queryset.filter( consultation__patient__facility__district=self.request.user.district ) @@ -206,8 +204,8 @@ def get_sessions(self, request, *args, **kwargs): responses={204: "Operation successful"}, tags=["investigation"], ) - @action(detail=False, methods=["PUT"]) - def batchUpdate(self, request, *args, **kwargs): + @action(detail=False, methods=["PUT"], url_path="batchUpdate") + def batch_update(self, request, *args, **kwargs): if "investigations" not in request.data: return Response( {"investigation": "is required"}, diff --git a/care/facility/api/viewsets/patient_otp.py b/care/facility/api/viewsets/patient_otp.py index ca88d83793..bd857fcf7f 100644 --- a/care/facility/api/viewsets/patient_otp.py +++ b/care/facility/api/viewsets/patient_otp.py @@ -1,5 +1,3 @@ -from re import error - from django.conf import settings from drf_spectacular.utils import extend_schema, extend_schema_view from rest_framework import mixins @@ -28,13 +26,16 @@ class PatientMobileOTPViewSet( @action(detail=False, methods=["POST"]) def login(self, request): if "phone_number" not in request.data or "otp" not in request.data: - raise ValidationError("Request Incomplete") + msg = "Request Incomplete" + raise ValidationError(msg) phone_number = request.data["phone_number"] otp = request.data["otp"] try: mobile_validator(phone_number) - except error: - raise ValidationError({"phone_number": "Invalid phone number format"}) + except Exception as e: + raise ValidationError( + {"phone_number": "Invalid phone number format"} + ) from e if len(otp) != settings.OTP_LENGTH: raise ValidationError({"otp": "Invalid OTP"}) @@ -47,7 +48,6 @@ def login(self, request): otp_object.is_used = True otp_object.save() - # return JWT token = PatientToken() token["phone_number"] = phone_number diff --git a/care/facility/api/viewsets/patient_otp_data.py b/care/facility/api/viewsets/patient_otp_data.py index 76647ae5c5..b0f5e6d234 100644 --- a/care/facility/api/viewsets/patient_otp_data.py +++ b/care/facility/api/viewsets/patient_otp_data.py @@ -28,5 +28,4 @@ def get_queryset(self): def get_serializer_class(self): if self.action == "list": return PatientListSerializer - else: - return self.serializer_class + return self.serializer_class diff --git a/care/facility/api/viewsets/patient_sample.py b/care/facility/api/viewsets/patient_sample.py index 0c698c95a8..279fd5fcd5 100644 --- a/care/facility/api/viewsets/patient_sample.py +++ b/care/facility/api/viewsets/patient_sample.py @@ -93,7 +93,7 @@ def get_serializer_class(self): return serializer_class def get_queryset(self): - queryset = super(PatientSampleViewSet, self).get_queryset() + queryset = super().get_queryset() if self.kwargs.get("patient_external_id") is not None: queryset = queryset.filter( patient__external_id=self.kwargs.get("patient_external_id") @@ -118,7 +118,7 @@ def list(self, request, *args, **kwargs): not self.kwargs.get("patient_external_id") and request.user.user_type < User.TYPE_VALUE_MAP["Doctor"] ): - raise PermissionDenied() + raise PermissionDenied if settings.CSV_REQUEST_PARAMETER in request.GET: queryset = ( @@ -131,7 +131,7 @@ def list(self, request, *args, **kwargs): field_header_map=PatientSample.CSV_MAPPING, field_serializer_map=PatientSample.CSV_MAKE_PRETTY, ) - return super(PatientSampleViewSet, self).list(request, *args, **kwargs) + return super().list(request, *args, **kwargs) def perform_create(self, serializer): validated_data = serializer.validated_data diff --git a/care/facility/api/viewsets/prescription.py b/care/facility/api/viewsets/prescription.py index fe8914098c..8e67aa5beb 100644 --- a/care/facility/api/viewsets/prescription.py +++ b/care/facility/api/viewsets/prescription.py @@ -184,7 +184,6 @@ def administer(self, request, *args, **kwargs): class MedibaseViewSet(ViewSet): - def serialize_data(self, objects: list[MedibaseMedicine]): return [medicine.get_representation() for medicine in objects] @@ -195,8 +194,8 @@ def list(self, request): limit = 30 query = [] - if type := request.query_params.get("type"): - query.append(MedibaseMedicine.type == type) + if t := request.query_params.get("type"): + query.append(MedibaseMedicine.type == t) if q := request.query_params.get("query"): query.append( diff --git a/care/facility/api/viewsets/resources.py b/care/facility/api/viewsets/resources.py index 711f7fcac2..2197e7123f 100644 --- a/care/facility/api/viewsets/resources.py +++ b/care/facility/api/viewsets/resources.py @@ -46,7 +46,7 @@ def get_request_queryset(request, queryset): q_objects |= Q(approving_facility__state=request.user.state) q_objects |= Q(assigned_facility__state=request.user.state) return queryset.filter(q_objects) - elif request.user.user_type >= User.TYPE_VALUE_MAP["DistrictLabAdmin"]: + if request.user.user_type >= User.TYPE_VALUE_MAP["DistrictLabAdmin"]: q_objects = Q(origin_facility__district=request.user.district) q_objects |= Q(approving_facility__district=request.user.district) q_objects |= Q(assigned_facility__district=request.user.district) @@ -165,7 +165,7 @@ def get_queryset(self): request__assigned_facility__state=self.request.user.state ) return queryset.filter(q_objects) - elif self.request.user.user_type >= User.TYPE_VALUE_MAP["DistrictLabAdmin"]: + if self.request.user.user_type >= User.TYPE_VALUE_MAP["DistrictLabAdmin"]: q_objects = Q( request__origin_facility__district=self.request.user.district ) diff --git a/care/facility/api/viewsets/shifting.py b/care/facility/api/viewsets/shifting.py index 3111e70c30..500da9fafb 100644 --- a/care/facility/api/viewsets/shifting.py +++ b/care/facility/api/viewsets/shifting.py @@ -15,6 +15,7 @@ from rest_framework.viewsets import GenericViewSet from care.facility.api.serializers.shifting import ( + REVERSE_SHIFTING_STATUS_CHOICES, ShiftingDetailSerializer, ShiftingListSerializer, ShiftingRequestCommentDetailSerializer, @@ -161,33 +162,38 @@ def get_serializer_class(self): @action(detail=True, methods=["POST"]) def transfer(self, request, *args, **kwargs): shifting_obj = self.get_object() - if has_facility_permission( - request.user, shifting_obj.shifting_approving_facility - ) or has_facility_permission(request.user, shifting_obj.assigned_facility): - if shifting_obj.assigned_facility and shifting_obj.status >= 70: - if shifting_obj.patient: - patient = shifting_obj.patient - patient.facility = shifting_obj.assigned_facility - patient.is_active = True - patient.allow_transfer = False - patient.save() - shifting_obj.status = 80 - shifting_obj.save(update_fields=["status"]) - # Discharge from all other active consultations - PatientConsultation.objects.filter( - patient=patient, discharge_date__isnull=True - ).update( - discharge_date=localtime(now()), - new_discharge_reason=NewDischargeReasonEnum.REFERRED, - ) - ConsultationBed.objects.filter( - consultation=patient.last_consultation, - end_date__isnull=True, - ).update(end_date=localtime(now())) + if ( + ( + has_facility_permission( + request.user, shifting_obj.shifting_approving_facility + ) + or has_facility_permission(request.user, shifting_obj.assigned_facility) + ) + and shifting_obj.assigned_facility + and shifting_obj.status + >= REVERSE_SHIFTING_STATUS_CHOICES["TRANSFER IN PROGRESS"] + and shifting_obj.patient + ): + patient = shifting_obj.patient + patient.facility = shifting_obj.assigned_facility + patient.is_active = True + patient.allow_transfer = False + patient.save() + shifting_obj.status = REVERSE_SHIFTING_STATUS_CHOICES["COMPLETED"] + shifting_obj.save(update_fields=["status"]) + # Discharge from all other active consultations + PatientConsultation.objects.filter( + patient=patient, discharge_date__isnull=True + ).update( + discharge_date=localtime(now()), + new_discharge_reason=NewDischargeReasonEnum.REFERRED, + ) + ConsultationBed.objects.filter( + consultation=patient.last_consultation, + end_date__isnull=True, + ).update(end_date=localtime(now())) - return Response( - {"transfer": "completed"}, status=status.HTTP_200_OK - ) + return Response({"transfer": "completed"}, status=status.HTTP_200_OK) return Response( {"error": "Invalid Request"}, status=status.HTTP_400_BAD_REQUEST ) @@ -204,8 +210,7 @@ def list(self, request, *args, **kwargs): field_header_map=ShiftingRequest.CSV_MAPPING, field_serializer_map=ShiftingRequest.CSV_MAKE_PRETTY, ) - response = super().list(request, *args, **kwargs) - return response + return super().list(request, *args, **kwargs) class ShifitngRequestCommentViewSet( @@ -236,7 +241,7 @@ def get_queryset(self): request__assigned_facility__state=self.request.user.state ) return queryset.filter(q_objects) - elif self.request.user.user_type >= User.TYPE_VALUE_MAP["DistrictLabAdmin"]: + if self.request.user.user_type >= User.TYPE_VALUE_MAP["DistrictLabAdmin"]: q_objects = Q( request__origin_facility__district=self.request.user.district ) diff --git a/care/facility/api/viewsets/summary.py b/care/facility/api/viewsets/summary.py index e223a204c4..fbc60f6bb3 100644 --- a/care/facility/api/viewsets/summary.py +++ b/care/facility/api/viewsets/summary.py @@ -48,17 +48,6 @@ class FacilityCapacitySummaryViewSet( def list(self, request, *args, **kwargs): return super().list(request, *args, **kwargs) - # def get_queryset(self): - # user = self.request.user - # queryset = self.queryset - # if user.is_superuser: - # return queryset - # elif self.request.user.user_type >= User.TYPE_VALUE_MAP["DistrictReadOnlyAdmin"]: - # return queryset.filter(facility__district=user.district) - # elif self.request.user.user_type >= User.TYPE_VALUE_MAP["StateReadOnlyAdmin"]: - # return queryset.filter(facility__state=user.state) - # return queryset.filter(facility__users__id__exact=user.id) - class TriageSummaryViewSet(ListModelMixin, GenericViewSet): lookup_field = "external_id" @@ -71,17 +60,6 @@ class TriageSummaryViewSet(ListModelMixin, GenericViewSet): filter_backends = (filters.DjangoFilterBackend,) filterset_class = FacilitySummaryFilter - # def get_queryset(self): - # user = self.request.user - # queryset = self.queryset - # if user.is_superuser: - # return queryset - # elif self.request.user.user_type >= User.TYPE_VALUE_MAP["DistrictReadOnlyAdmin"]: - # return queryset.filter(facility__district=user.district) - # elif self.request.user.user_type >= User.TYPE_VALUE_MAP["StateReadOnlyAdmin"]: - # return queryset.filter(facility__state=user.state) - # return queryset.filter(facility__users__id__exact=user.id) - @extend_schema(tags=["summary"]) @method_decorator(cache_page(60 * 60)) def list(self, request, *args, **kwargs): @@ -99,17 +77,6 @@ class TestsSummaryViewSet(ListModelMixin, GenericViewSet): filter_backends = (filters.DjangoFilterBackend,) filterset_class = FacilitySummaryFilter - # def get_queryset(self): - # user = self.request.user - # queryset = self.queryset - # if user.is_superuser: - # return queryset - # elif self.request.user.user_type >= User.TYPE_VALUE_MAP["DistrictReadOnlyAdmin"]: - # return queryset.filter(facility__district=user.district) - # elif self.request.user.user_type >= User.TYPE_VALUE_MAP["StateReadOnlyAdmin"]: - # return queryset.filter(facility__state=user.state) - # return queryset.filter(facility__users__id__exact=user.id) - @extend_schema(tags=["summary"]) @method_decorator(cache_page(60 * 60 * 10)) def list(self, request, *args, **kwargs): @@ -132,17 +99,6 @@ class PatientSummaryViewSet(ListModelMixin, GenericViewSet): def list(self, request, *args, **kwargs): return super().list(request, *args, **kwargs) - # def get_queryset(self): - # user = self.request.user - # queryset = self.queryset - # if user.is_superuser: - # return queryset - # elif self.request.user.user_type >= User.TYPE_VALUE_MAP["DistrictReadOnlyAdmin"]: - # return queryset.filter(facility__district=user.district) - # elif self.request.user.user_type >= User.TYPE_VALUE_MAP["StateReadOnlyAdmin"]: - # return queryset.filter(facility__state=user.state) - # return queryset.filter(facility__users__id__exact=user.id) - class DistrictSummaryFilter(filters.FilterSet): start_date = filters.DateFilter(field_name="created_date", lookup_expr="gte") @@ -168,14 +124,3 @@ class DistrictPatientSummaryViewSet(ListModelMixin, GenericViewSet): @method_decorator(cache_page(60 * 10)) def list(self, request, *args, **kwargs): return super().list(request, *args, **kwargs) - - # def get_queryset(self): - # user = self.request.user - # queryset = self.queryset - # if user.is_superuser: - # return queryset - # elif self.request.user.user_type >= User.TYPE_VALUE_MAP["DistrictReadOnlyAdmin"]: - # return queryset.filter(facility__district=user.district) - # elif self.request.user.user_type >= User.TYPE_VALUE_MAP["StateReadOnlyAdmin"]: - # return queryset.filter(facility__state=user.state) - # return queryset.filter(facility__users__id__exact=user.id) diff --git a/care/facility/events/handler.py b/care/facility/events/handler.py index 53c3ffc6ba..a674c39e8e 100644 --- a/care/facility/events/handler.py +++ b/care/facility/events/handler.py @@ -25,7 +25,7 @@ def create_consultation_event_entry( fields: set[str] = ( get_changed_fields(old_instance, object_instance) if old_instance - else {field.name for field in object_instance._meta.fields} + else {field.name for field in object_instance._meta.fields} # noqa: SLF001 ) fields_to_store = fields_to_store & fields if fields_to_store else fields @@ -91,11 +91,10 @@ def create_consultation_events( taken_at = created_date with transaction.atomic(): - if isinstance(objects, (QuerySet, list, tuple)): + if isinstance(objects, QuerySet | list | tuple): if old is not None: - raise ValueError( - "diff is not available when objects is a list or queryset" - ) + msg = "diff is not available when objects is a list or queryset" + raise ValueError(msg) for obj in objects: create_consultation_event_entry( consultation_id, diff --git a/care/facility/management/commands/add_daily_round_consultation.py b/care/facility/management/commands/add_daily_round_consultation.py index 680d01cd95..d6732e1480 100644 --- a/care/facility/management/commands/add_daily_round_consultation.py +++ b/care/facility/management/commands/add_daily_round_consultation.py @@ -11,17 +11,18 @@ class Command(BaseCommand): help = "Populate daily round for consultations" def handle(self, *args, **options): + batch_size = 10000 consultations = list( PatientConsultation.objects.filter( last_daily_round__isnull=True ).values_list("external_id") ) total_count = len(consultations) - print(f"{total_count} Consultations need to be updated") + self.stdout.write(f"{total_count} Consultations need to be updated") i = 0 for consultation_eid in consultations: - if i > 10000 and i % 10000 == 0: - print(f"{i} operations performed") + if i > batch_size and i % batch_size == 0: + self.stdout.write(f"{i} operations performed") i = i + 1 PatientConsultation.objects.filter(external_id=consultation_eid[0]).update( last_daily_round=DailyRound.objects.filter( @@ -30,4 +31,4 @@ def handle(self, *args, **options): .order_by("-created_date") .first() ) - print("Operation Completed") + self.stdout.write("Operation Completed") diff --git a/care/facility/management/commands/clean_patient_phone_numbers.py b/care/facility/management/commands/clean_patient_phone_numbers.py index e95bb23686..199cb79ef9 100644 --- a/care/facility/management/commands/clean_patient_phone_numbers.py +++ b/care/facility/management/commands/clean_patient_phone_numbers.py @@ -33,5 +33,5 @@ def handle(self, *args, **options) -> str | None: except Exception: failed.append({"id": patient.id, "phone_number": patient.phone_number}) - print(f"Completed for {qs.count()} | Failed for {len(failed)}") - print(f"Failed for {json.dumps(failed)}") + self.stdout.write(f"Completed for {qs.count()} | Failed for {len(failed)}") + self.stdout.write(f"Failed for {json.dumps(failed)}") diff --git a/care/facility/management/commands/generate_jwks.py b/care/facility/management/commands/generate_jwks.py index 750f82d46a..19f1a58fcc 100644 --- a/care/facility/management/commands/generate_jwks.py +++ b/care/facility/management/commands/generate_jwks.py @@ -11,4 +11,4 @@ class Command(BaseCommand): help = "Generate JWKS" def handle(self, *args, **options): - print(generate_encoded_jwks()) + self.stdout.write(generate_encoded_jwks()) diff --git a/care/facility/management/commands/load_dummy_data.py b/care/facility/management/commands/load_dummy_data.py index f42ac0ceef..4a0633801c 100644 --- a/care/facility/management/commands/load_dummy_data.py +++ b/care/facility/management/commands/load_dummy_data.py @@ -17,9 +17,8 @@ class Command(BaseCommand): def handle(self, *args, **options): env = os.environ.setdefault("DJANGO_SETTINGS_MODULE", "config.settings.local") if "production" in env or "staging" in env: - raise CommandError( - "This command is not intended to be run in production environment." - ) + msg = "This command is not intended to be run in production environment." + raise CommandError(msg) try: management.call_command("loaddata", self.BASE_URL + "states.json") @@ -32,4 +31,4 @@ def handle(self, *args, **options): ) management.call_command("populate_investigations") except Exception as e: - raise CommandError(e) + raise CommandError(e) from e diff --git a/care/facility/management/commands/load_event_types.py b/care/facility/management/commands/load_event_types.py index 271f510b79..e0999e21aa 100644 --- a/care/facility/management/commands/load_event_types.py +++ b/care/facility/management/commands/load_event_types.py @@ -271,7 +271,10 @@ class Command(BaseCommand): ) def create_objects( - self, types: tuple[EventType, ...], model: str = None, parent: EventType = None + self, + types: tuple[EventType, ...], + model: str | None = None, + parent: EventType = None, ): for event_type in types: model = event_type.get("model", model) diff --git a/care/facility/management/commands/load_icd11_diagnoses_data.py b/care/facility/management/commands/load_icd11_diagnoses_data.py index 1354c40c94..9f3343a370 100644 --- a/care/facility/management/commands/load_icd11_diagnoses_data.py +++ b/care/facility/management/commands/load_icd11_diagnoses_data.py @@ -1,12 +1,18 @@ import json +from typing import TYPE_CHECKING +from django.conf import settings from django.core.management import BaseCommand, CommandError from care.facility.models.icd11_diagnosis import ICD11Diagnosis +if TYPE_CHECKING: + from pathlib import Path + def fetch_data(): - with open("data/icd11.json") as json_file: + icd11_json: Path = settings.BASE_DIR / "data" / "icd11.json" + with icd11_json.open() as json_file: return json.load(json_file) @@ -117,17 +123,17 @@ def my(x): # The following code is never executed as the `icd11.json` file is # pre-sorted and hence the parent is always present before the child. - print("Full-scan for", id, item["label"]) + self.stdout.write("Full-scan for", id, item["label"]) return self.find_roots( - [ + next( icd11_object for icd11_object in self.data if icd11_object["ID"] == item["parentId"] - ][0] + ) ) def handle(self, *args, **options): - print("Loading ICD11 diagnoses data to database...") + self.stdout.write("Loading ICD11 diagnoses data to database...") try: self.data = fetch_data() @@ -162,4 +168,4 @@ def roots(item): ignore_conflicts=True, # Voluntarily set to skip duplicates, so that we can run this command multiple times + existing relations are not affected ) except Exception as e: - raise CommandError(e) + raise CommandError(e) from e diff --git a/care/facility/management/commands/load_medicines_data.py b/care/facility/management/commands/load_medicines_data.py index 8f909a7424..4dbbfb245a 100644 --- a/care/facility/management/commands/load_medicines_data.py +++ b/care/facility/management/commands/load_medicines_data.py @@ -1,9 +1,14 @@ import json +from typing import TYPE_CHECKING +from django.conf import settings from django.core.management import BaseCommand from care.facility.models import MedibaseMedicine +if TYPE_CHECKING: + from pathlib import Path + class Command(BaseCommand): """ @@ -14,11 +19,14 @@ class Command(BaseCommand): help = "Loads Medibase Medicines into the database from medibase.json" def fetch_data(self): - with open("data/medibase.json") as json_file: + medibase_json: Path = settings.BASE_DIR / "data" / "medibase.json" + with medibase_json.open() as json_file: return json.load(json_file) def handle(self, *args, **options): - print("Loading Medibase Medicines into the database from medibase.json") + self.stdout.write( + "Loading Medibase Medicines into the database from medibase.json" + ) medibase_objects = self.fetch_data() MedibaseMedicine.objects.bulk_create( diff --git a/care/facility/management/commands/load_redis_index.py b/care/facility/management/commands/load_redis_index.py index da31525d34..736f482836 100644 --- a/care/facility/management/commands/load_redis_index.py +++ b/care/facility/management/commands/load_redis_index.py @@ -18,10 +18,10 @@ class Command(BaseCommand): def handle(self, *args, **options): if cache.get("redis_index_loading"): - print("Redis Index already loading, skipping") + self.stdout.write("Redis Index already loading, skipping") return - cache.set("redis_index_loading", True, timeout=60 * 5) + cache.set("redis_index_loading", value=True, timeout=60 * 5) load_icd11_diagnosis() load_medibase_medicines() @@ -35,8 +35,8 @@ def handle(self, *args, **options): if load_static_data: load_static_data() except ModuleNotFoundError: - print(f"Module {module_path} not found") + self.stdout.write(f"Module {module_path} not found") except Exception as e: - print(f"Error loading static data for {plug.name}: {e}") + self.stdout.write(f"Error loading static data for {plug.name}: {e}") cache.delete("redis_index_loading") diff --git a/care/facility/management/commands/port_patient_wards.py b/care/facility/management/commands/port_patient_wards.py index bde986cbfb..bf6b558d1f 100644 --- a/care/facility/management/commands/port_patient_wards.py +++ b/care/facility/management/commands/port_patient_wards.py @@ -32,7 +32,7 @@ def handle(self, *args, **options): patient.save() except Exception: failed += 1 - print( + self.stdout.write( str(failed), " failed operations ", str(success), diff --git a/care/facility/management/commands/summarize.py b/care/facility/management/commands/summarize.py index bc77fd5d03..d008e21df5 100644 --- a/care/facility/management/commands/summarize.py +++ b/care/facility/management/commands/summarize.py @@ -1,12 +1,12 @@ from django.core.management.base import BaseCommand -from care.facility.utils.summarisation.district.patient_summary import ( +from care.facility.utils.summarization.district.patient_summary import ( district_patient_summary, ) -from care.facility.utils.summarisation.facility_capacity import ( +from care.facility.utils.summarization.facility_capacity import ( facility_capacity_summary, ) -from care.facility.utils.summarisation.patient_summary import patient_summary +from care.facility.utils.summarization.patient_summary import patient_summary class Command(BaseCommand): @@ -18,8 +18,8 @@ class Command(BaseCommand): def handle(self, *args, **options): patient_summary() - print("Patients Summarised") + self.stdout.write("Patients Summarised") facility_capacity_summary() - print("Capacity Summarised") + self.stdout.write("Capacity Summarised") district_patient_summary() - print("District Wise Patient Summarised") + self.stdout.write("District Wise Patient Summarised") diff --git a/care/facility/management/commands/sync_external_test_patient.py b/care/facility/management/commands/sync_external_test_patient.py index 08be07911d..ad0fe27f55 100644 --- a/care/facility/management/commands/sync_external_test_patient.py +++ b/care/facility/management/commands/sync_external_test_patient.py @@ -12,10 +12,10 @@ class Command(BaseCommand): help = "Sync the patient created flag in external tests" def handle(self, *args, **options): - print("Starting Sync") + self.stdout.write("Starting Sync") for patient in PatientRegistration.objects.all(): if patient.srf_id: PatientExternalTest.objects.filter( srf_id__iexact=patient.srf_id ).update(patient_created=True) - print("Completed Sync") + self.stdout.write("Completed Sync") diff --git a/care/facility/management/commands/sync_patient_age.py b/care/facility/management/commands/sync_patient_age.py index 8b2ff40321..72495ca2d8 100644 --- a/care/facility/management/commands/sync_patient_age.py +++ b/care/facility/management/commands/sync_patient_age.py @@ -19,6 +19,6 @@ def handle(self, *args, **options): except Exception: failed += 1 if failed: - print(f"Failed for {failed} Patient") + self.stdout.write(f"Failed for {failed} Patient") else: - print("Successfully Synced Age") + self.stdout.write("Successfully Synced Age") diff --git a/care/facility/models/ambulance.py b/care/facility/models/ambulance.py index e0b7a07751..90040ddc00 100644 --- a/care/facility/models/ambulance.py +++ b/care/facility/models/ambulance.py @@ -32,10 +32,6 @@ class Ambulance(FacilityBaseModel): ) owner_is_smart_phone = models.BooleanField(default=True) - # primary_district = models.IntegerField(choices=DISTRICT_CHOICES, blank=False) - # secondary_district = models.IntegerField(choices=DISTRICT_CHOICES, blank=True, null=True) - # third_district = models.IntegerField(choices=DISTRICT_CHOICES, blank=True, null=True) - primary_district = models.ForeignKey( District, on_delete=models.PROTECT, @@ -122,15 +118,6 @@ def has_object_update_permission(self, request): ) ) - # class Meta: - # constraints = [ - # models.CheckConstraint( - # name="ambulance_free_or_price", - # check=models.Q(price_per_km__isnull=False) - # | models.Q(has_free_service=True), - # ) - # ] - class AmbulanceDriver(FacilityBaseModel): ambulance = models.ForeignKey(Ambulance, on_delete=models.CASCADE) diff --git a/care/facility/models/asset.py b/care/facility/models/asset.py index 7cb027283d..bf2ab5e317 100644 --- a/care/facility/models/asset.py +++ b/care/facility/models/asset.py @@ -62,7 +62,7 @@ class AssetType(enum.Enum): AssetTypeChoices = [(e.value, e.name) for e in AssetType] -AssetClassChoices = [(e.name, e.value._name) for e in AssetClasses] +AssetClassChoices = [(e.name, e.value._name) for e in AssetClasses] # noqa: SLF001 class Status(enum.Enum): @@ -162,6 +162,7 @@ def resolved_middleware(self): "hostname": hostname, "source": "facility", } + return None class Meta: constraints = [ @@ -312,3 +313,6 @@ class AssetServiceEdit(models.Model): class Meta: ordering = ["-edited_on"] + + def __str__(self): + return f"{self.asset_service.asset.name} - {self.serviced_on}" diff --git a/care/facility/models/daily_round.py b/care/facility/models/daily_round.py index 718b75d058..edc11a8954 100644 --- a/care/facility/models/daily_round.py +++ b/care/facility/models/daily_round.py @@ -571,7 +571,7 @@ def save(self, *args, **kwargs): if self.output is not None: self.total_output_calculated = sum([x["quantity"] for x in self.output]) - super(DailyRound, self).save(*args, **kwargs) + super().save(*args, **kwargs) @staticmethod def has_read_permission(request): @@ -585,8 +585,8 @@ def has_read_permission(request): return request.user.is_superuser or ( (request.user in consultation.patient.facility.users.all()) or ( - request.user == consultation.assigned_to - or request.user == consultation.patient.assigned_to + request.user + in (consultation.assigned_to, consultation.patient.assigned_to) ) or ( request.user.user_type >= User.TYPE_VALUE_MAP["DistrictLabAdmin"] @@ -620,8 +620,11 @@ def has_object_read_permission(self, request): and request.user in self.consultation.patient.facility.users.all() ) or ( - self.consultation.assigned_to == request.user - or request.user == self.consultation.patient.assigned_to + request.user + in ( + self.consultation.assigned_to, + self.consultation.patient.assigned_to, + ) ) or ( request.user.user_type >= User.TYPE_VALUE_MAP["DistrictLabAdmin"] diff --git a/care/facility/models/encounter_symptom.py b/care/facility/models/encounter_symptom.py index 9412927aff..f7b1d6dd63 100644 --- a/care/facility/models/encounter_symptom.py +++ b/care/facility/models/encounter_symptom.py @@ -83,7 +83,8 @@ class EncounterSymptom(BaseModel, ConsultationRelatedPermissionMixin): def save(self, *args, **kwargs): if self.other_symptom and self.symptom != Symptom.OTHERS: - raise ValueError("Other Symptom should be empty when Symptom is not OTHERS") + msg = "Other Symptom should be empty when Symptom is not OTHERS" + raise ValueError(msg) if self.clinical_impression_status != ClinicalImpressionStatus.ENTERED_IN_ERROR: if self.onset_date and self.cure_date: diff --git a/care/facility/models/events.py b/care/facility/models/events.py index 9b7fbfc56a..7499837138 100644 --- a/care/facility/models/events.py +++ b/care/facility/models/events.py @@ -31,19 +31,19 @@ class EventType(models.Model): created_date = models.DateTimeField(auto_now_add=True) is_active = models.BooleanField(default=True) - def get_descendants(self): - descendants = list(self.children.all()) - for child in self.children.all(): - descendants.extend(child.get_descendants()) - return descendants + def __str__(self) -> str: + return f"{self.model} - {self.name}" def save(self, *args, **kwargs): if self.description is not None and not self.description.strip(): self.description = None return super().save(*args, **kwargs) - def __str__(self) -> str: - return f"{self.model} - {self.name}" + def get_descendants(self): + descendants = list(self.children.all()) + for child in self.children.all(): + descendants.extend(child.get_descendants()) + return descendants class PatientConsultationEvent(models.Model): @@ -68,16 +68,9 @@ class PatientConsultationEvent(models.Model): max_length=10, choices=ChangeType, default=ChangeType.CREATED ) - def __str__(self) -> str: - return f"{self.id} - {self.consultation_id} - {self.event_type} - {self.change_type}" - class Meta: ordering = ["-created_date"] indexes = [models.Index(fields=["consultation", "is_latest"])] - # constraints = [ - # models.UniqueConstraint( - # fields=["consultation", "event_type", "is_latest"], - # condition=models.Q(is_latest=True), - # name="unique_consultation_event_type_is_latest", - # ) - # ] + + def __str__(self) -> str: + return f"{self.id} - {self.consultation_id} - {self.event_type} - {self.change_type}" diff --git a/care/facility/models/facility.py b/care/facility/models/facility.py index fcc2408114..e3593871b7 100644 --- a/care/facility/models/facility.py +++ b/care/facility/models/facility.py @@ -90,27 +90,27 @@ class FacilityFeature(models.IntegerChoices): (5, "Hotel"), (6, "Lodge"), (7, "TeleMedicine"), - # (8, "Govt Hospital"), # Change from "Govt Hospital" to "Govt Medical College Hospitals" + # 8, "Govt Hospital" # Change from "Govt Hospital" to "Govt Medical College Hospitals" (9, "Govt Labs"), (10, "Private Labs"), # Use 8xx for Govt owned hospitals and health centres (800, "Primary Health Centres"), - # (801, "24x7 Public Health Centres"), # Change from "24x7 Public Health Centres" to "Primary Health Centres" + # 801, "24x7 Public Health Centres" # Change from "24x7 Public Health Centres" to "Primary Health Centres" (802, "Family Health Centres"), (803, "Community Health Centres"), - # (820, "Urban Primary Health Center"), # Change from "Urban Primary Health Center" to "Primary Health Centres" + # 820, "Urban Primary Health Center" # Change from "Urban Primary Health Center" to "Primary Health Centres" (830, "Taluk Hospitals"), - # (831, "Taluk Headquarters Hospitals"), # Change from "Taluk Headquarters Hospitals" to "Taluk Hospitals" + # 831, "Taluk Headquarters Hospitals" # Change from "Taluk Headquarters Hospitals" to "Taluk Hospitals" (840, "Women and Child Health Centres"), - # (850, "General hospitals"), # Change from "General hospitals" to "District Hospitals" + # 850, "General hospitals" # Change from "General hospitals" to "District Hospitals" (860, "District Hospitals"), (870, "Govt Medical College Hospitals"), (900, "Co-operative hospitals"), (910, "Autonomous healthcare facility"), # Use 9xx for Labs - # (950, "Corona Testing Labs"), # Change from "Corona Testing Labs" to "Govt Labs" + # 950, "Corona Testing Labs" # Change from "Corona Testing Labs" to "Govt Labs" # Use 10xx for Corona Care Center - # (1000, "Corona Care Centre"), # Change from "Corona Care Centre" to "Other" + # 1000, "Corona Care Centre" # Change from "Corona Care Centre" to "Other" (1010, "COVID-19 Domiciliary Care Center"), # Use 11xx for First Line Treatment Centre (1100, "First Line Treatment Centre"), @@ -207,7 +207,8 @@ def check_if_spoke_is_not_ancestor(base_id: int, spoke_id: int): "hub_id", flat=True ) if spoke_id in ancestors_of_base: - raise serializers.ValidationError("This facility is already an ancestor hub") + msg = "This facility is already an ancestor hub" + raise serializers.ValidationError(msg) for ancestor in ancestors_of_base: check_if_spoke_is_not_ancestor(ancestor, spoke_id) diff --git a/care/facility/models/file_upload.py b/care/facility/models/file_upload.py index 4093e15e53..1176044635 100644 --- a/care/facility/models/file_upload.py +++ b/care/facility/models/file_upload.py @@ -46,10 +46,6 @@ class FileCategory(models.TextChoices): class Meta: abstract = True - def delete(self, *args): - self.deleted = True - self.save(update_fields=["deleted"]) - def save(self, *args, **kwargs): if "force_insert" in kwargs or (not self.internal_name): internal_name = str(uuid4()) + str(int(time.time())) @@ -60,6 +56,10 @@ def save(self, *args, **kwargs): self.internal_name = internal_name return super().save(*args, **kwargs) + def delete(self, *args): + self.deleted = True + self.save(update_fields=["deleted"]) + def get_extension(self): parts = self.internal_name.split(".") return f".{parts[-1]}" if len(parts) > 1 else "" @@ -67,7 +67,7 @@ def get_extension(self): def signed_url( self, duration=60 * 60, mime_type=None, bucket_type=BucketType.PATIENT ): - config, bucket_name = get_client_config(bucket_type, True) + config, bucket_name = get_client_config(bucket_type, external=True) s3 = boto3.client("s3", **config) params = { "Bucket": bucket_name, @@ -82,7 +82,7 @@ def signed_url( ) def read_signed_url(self, duration=60 * 60, bucket_type=BucketType.PATIENT): - config, bucket_name = get_client_config(bucket_type, True) + config, bucket_name = get_client_config(bucket_type, external=True) s3 = boto3.client("s3", **config) return s3.generate_presigned_url( "get_object", @@ -128,8 +128,6 @@ class FileUpload(BaseFileUpload): all data will be private and file access will be given on a NEED TO BASIS ONLY """ - # TODO : Periodic tasks that removes files that were never uploaded - class FileType(models.IntegerChoices): OTHER = 0, "OTHER" PATIENT = 1, "PATIENT" @@ -164,6 +162,9 @@ class FileType(models.IntegerChoices): FileTypeChoices = [(x.value, x.name) for x in FileType] FileCategoryChoices = [(x.value, x.name) for x in BaseFileUpload.FileCategory] + def __str__(self): + return f"{self.FileTypeChoices[self.file_type][1]} - {self.name}{' (Archived)' if self.is_archived else ''}" + def save(self, *args, **kwargs): from care.facility.models import PatientConsent @@ -192,7 +193,7 @@ def save(self, *args, **kwargs): ).exclude(pk=self.pk if self.is_archived else None) ) if not new_consent - else models.Value(True) + else models.Value(value=True) ) ) .filter(has_files=True) @@ -203,6 +204,3 @@ def save(self, *args, **kwargs): consultation.save() return super().save(*args, **kwargs) - - def __str__(self): - return f"{self.FileTypeChoices[self.file_type][1]} - {self.name}{' (Archived)' if self.is_archived else ''}" diff --git a/care/utils/serializer/__init__.py b/care/facility/models/json_schema/__init__.py similarity index 100% rename from care/utils/serializer/__init__.py rename to care/facility/models/json_schema/__init__.py diff --git a/care/facility/models/mixins/permissions/patient.py b/care/facility/models/mixins/permissions/patient.py index b814cde15a..82410aabf3 100644 --- a/care/facility/models/mixins/permissions/patient.py +++ b/care/facility/models/mixins/permissions/patient.py @@ -19,9 +19,9 @@ def has_object_read_permission(self, request): doctor_allowed = False if self.last_consultation: - doctor_allowed = ( - self.last_consultation.assigned_to == request.user - or request.user == self.assigned_to + doctor_allowed = request.user in ( + self.last_consultation.assigned_to, + self.assigned_to, ) return request.user.is_superuser or ( (hasattr(self, "created_by") and request.user == self.created_by) @@ -60,9 +60,9 @@ def has_object_write_permission(self, request): doctor_allowed = False if self.last_consultation: - doctor_allowed = ( - self.last_consultation.assigned_to == request.user - or request.user == self.assigned_to + doctor_allowed = request.user in ( + self.last_consultation.assigned_to, + self.assigned_to, ) return request.user.is_superuser or ( @@ -99,9 +99,9 @@ def has_object_update_permission(self, request): doctor_allowed = False if self.last_consultation: - doctor_allowed = ( - self.last_consultation.assigned_to == request.user - or request.user == self.assigned_to + doctor_allowed = request.user in ( + self.last_consultation.assigned_to, + self.assigned_to, ) return ( diff --git a/care/facility/models/patient.py b/care/facility/models/patient.py index f635a773d4..4f8d54c971 100644 --- a/care/facility/models/patient.py +++ b/care/facility/models/patient.py @@ -69,7 +69,7 @@ class SourceEnum(enum.Enum): SourceChoices = [(e.value, e.name) for e in SourceEnum] - class vaccineEnum(enum.Enum): + class VaccineEnum(enum.Enum): COVISHIELD = "CoviShield" COVAXIN = "Covaxin" SPUTNIK = "Sputnik" @@ -78,7 +78,7 @@ class vaccineEnum(enum.Enum): JANSSEN = "Janssen" SINOVAC = "Sinovac" - vaccineChoices = [(e.value, e.name) for e in vaccineEnum] + VaccineChoices = [(e.value, e.name) for e in VaccineEnum] class ActionEnum(enum.Enum): NO_ACTION = 10 @@ -116,12 +116,10 @@ class TestTypeEnum(enum.Enum): "PatientMetaInfo", on_delete=models.SET_NULL, null=True ) - # name_old = EncryptedCharField(max_length=200, default="") name = models.CharField(max_length=200, default="") gender = models.IntegerField(choices=GENDER_CHOICES, blank=False) - # phone_number_old = EncryptedCharField(max_length=14, validators=[phone_number_regex], default="") phone_number = models.CharField( max_length=14, validators=[mobile_or_landline_number_validator], default="" ) @@ -130,7 +128,6 @@ class TestTypeEnum(enum.Enum): max_length=14, validators=[mobile_or_landline_number_validator], default="" ) - # address_old = EncryptedTextField(default="") address = models.TextField(default="") permanent_address = models.TextField(default="") @@ -205,7 +202,7 @@ class TestTypeEnum(enum.Enum): blank=True, verbose_name="Already pescribed medication if any", ) - has_SARI = models.BooleanField( + has_SARI = models.BooleanField( # noqa: N815 default=False, verbose_name="Does the Patient Suffer from SARI" ) @@ -383,7 +380,7 @@ class TestTypeEnum(enum.Enum): validators=[MinValueValidator(0), MaxValueValidator(3)], ) vaccine_name = models.CharField( - choices=vaccineChoices, + choices=VaccineChoices, default=None, null=True, blank=False, @@ -487,19 +484,17 @@ def get_age(self) -> str: year_str = f"{delta.years} year{pluralize(delta.years)}" return f"{year_str}" - elif delta.months > 0: + if delta.months > 0: month_str = f"{delta.months} month{pluralize(delta.months)}" day_str = ( f" {delta.days} day{pluralize(delta.days)}" if delta.days > 0 else "" ) return f"{month_str}{day_str}" - elif delta.days > 0: - day_str = f"{delta.days} day{pluralize(delta.days)}" - return day_str + if delta.days > 0: + return f"{delta.days} day{pluralize(delta.days)}" - else: - return "0 days" + return "0 days" def annotate_diagnosis_ids(*args, **kwargs): return ArrayAgg( @@ -552,14 +547,14 @@ def annotate_diagnosis_ids(*args, **kwargs): "last_consultation__discharge_date__time": "Time of discharge", } - def format_as_date(date): - return date.strftime("%d/%m/%Y") + def format_as_date(self): + return self.strftime("%d/%m/%Y") - def format_as_time(time): - return time.strftime("%H:%M") + def format_as_time(self): + return self.strftime("%H:%M") - def format_diagnoses(diagnosis_ids): - diagnoses = get_icd11_diagnoses_objects_by_ids(diagnosis_ids) + def format_diagnoses(self): + diagnoses = get_icd11_diagnoses_objects_by_ids(self) return ", ".join([diagnosis["label"] for diagnosis in diagnoses]) CSV_MAKE_PRETTY = { @@ -645,6 +640,9 @@ class DomesticHealthcareSupport(models.IntegerChoices): ) head_of_household = models.BooleanField(blank=True, null=True) + def __str__(self): + return f"PatientMetaInfo - {self.id}" + class PatientContactDetails(models.Model): class RelationEnum(enum.IntEnum): @@ -660,25 +658,23 @@ class RelationEnum(enum.IntEnum): OTHERS = 10 class ModeOfContactEnum(enum.IntEnum): - # "1. Touched body fluids of the patient (respiratory tract secretions/blood/vomit/saliva/urine/faces)" + # Touched body fluids of the patient (respiratory tract secretions/blood/vomit/saliva/urine/faces) TOUCHED_BODY_FLUIDS = 1 - # "2. Had direct physical contact with the body of the patient - # including physical examination without full precautions." + # Had direct physical contact with the body of the patient including physical examination without full precautions. DIRECT_PHYSICAL_CONTACT = 2 - # "3. Touched or cleaned the linens/clothes/or dishes of the patient" + # Touched or cleaned the linens/clothes/or dishes of the patient CLEANED_USED_ITEMS = 3 - # "4. Lives in the same household as the patient." + # Lives in the same household as the patient. LIVE_IN_SAME_HOUSEHOLD = 4 - # "5. Close contact within 3ft (1m) of the confirmed case without precautions." + # Close contact within 3ft (1m) of the confirmed case without precautions. CLOSE_CONTACT_WITHOUT_PRECAUTION = 5 - # "6. Passenger of the aeroplane with a confirmed COVID -19 passenger for more than 6 hours." + # Passenger of the aeroplane with a confirmed COVID -19 passenger for more than 6 hours. CO_PASSENGER_AEROPLANE = 6 - # "7. Health care workers and other contacts who had full PPE while handling the +ve case" + # Health care workers and other contacts who had full PPE while handling the +ve case HEALTH_CARE_WITH_PPE = 7 - # "8. Shared the same space(same class for school/worked in - # same room/similar and not having a high risk exposure" + # Shared the same space(same class for school/worked in same room/similar and not having a high risk exposure SHARED_SAME_SPACE_WITHOUT_HIGH_EXPOSURE = 8 - # "9. Travel in the same environment (bus/train/Flight) but not having a high-risk exposure as cited above." + # Travel in the same environment (bus/train/Flight) but not having a high-risk exposure as cited above. TRAVELLED_TOGETHER_WITHOUT_HIGH_EXPOSURE = 9 RelationChoices = [(item.value, item.name) for item in RelationEnum] @@ -709,6 +705,9 @@ class ModeOfContactEnum(enum.IntEnum): objects = BaseManager() + def __str__(self): + return f"{self.patient.name} - {self.patient_in_contact.name} - {self.get_relation_with_patient_display()}" + class Disease(models.Model): patient = models.ForeignKey( @@ -833,6 +832,9 @@ class PatientNotesEdit(models.Model): class Meta: ordering = ["-edited_date"] + def __str__(self): + return f"PatientNotesEdit {self.patient_note} - {self.edited_by}" + class PatientAgeFunc(Func): """ diff --git a/care/facility/models/patient_consultation.py b/care/facility/models/patient_consultation.py index bf241779a0..f21d4f6a4d 100644 --- a/care/facility/models/patient_consultation.py +++ b/care/facility/models/patient_consultation.py @@ -247,15 +247,6 @@ def get_related_consultation(self): ), } - # CSV_DATATYPE_DEFAULT_MAPPING = { - # "encounter_date": (None, models.DateTimeField(),), - # "deprecated_symptoms_onset_date": (None, models.DateTimeField(),), - # "deprecated_symptoms": ("-", models.CharField(),), - # "category": ("-", models.CharField(),), - # "examination_details": ("-", models.CharField(),), - # "suggestion": ("-", models.CharField(),), - # } - def __str__(self): return f"{self.patient.name}<>{self.facility.name}" @@ -271,7 +262,7 @@ def save(self, *args, **kwargs): if self.death_datetime and self.patient.death_datetime != self.death_datetime: self.patient.death_datetime = self.death_datetime self.patient.save(update_fields=["death_datetime"]) - super(PatientConsultation, self).save(*args, **kwargs) + super().save(*args, **kwargs) class Meta: constraints = [ @@ -299,10 +290,7 @@ def has_object_read_permission(self, request): self.patient.facility and request.user in self.patient.facility.users.all() ) - or ( - self.assigned_to == request.user - or request.user == self.patient.assigned_to - ) + or (request.user in (self.assigned_to, self.patient.assigned_to)) or ( request.user.user_type >= User.TYPE_VALUE_MAP["DistrictLabAdmin"] and ( @@ -352,6 +340,9 @@ class ConsultationClinician(models.Model): on_delete=models.PROTECT, ) + def __str__(self): + return f"ConsultationClinician {self.consultation} - {self.clinician}" + class PatientConsent(BaseModel, ConsultationRelatedPermissionMixin): consultation = models.ForeignKey( @@ -433,8 +424,11 @@ def has_object_read_permission(self, request): and request.user in self.consultation.patient.facility.users.all() ) or ( - self.consultation.assigned_to == request.user - or request.user == self.consultation.patient.assigned_to + request.user + in ( + self.consultation.assigned_to, + self.consultation.patient.assigned_to, + ) ) or ( request.user.user_type >= User.TYPE_VALUE_MAP["DistrictLabAdmin"] diff --git a/care/facility/models/patient_external_test.py b/care/facility/models/patient_external_test.py index 297ccbfd63..3d7ebfb292 100644 --- a/care/facility/models/patient_external_test.py +++ b/care/facility/models/patient_external_test.py @@ -79,7 +79,6 @@ class PatientExternalTest(FacilityBaseModel): "result": "Final Result", "sample_collection_date": "Sample Collection Date", "source": "Source", - # "result_date": "", } def __str__(self): diff --git a/care/facility/models/patient_icmr.py b/care/facility/models/patient_icmr.py index e6f06da451..23c78a7f4e 100644 --- a/care/facility/models/patient_icmr.py +++ b/care/facility/models/patient_icmr.py @@ -1,7 +1,6 @@ -import datetime - from dateutil.relativedelta import relativedelta from django.utils import timezone +from django.utils.timezone import now from care.facility.models import ( DISEASE_CHOICES_MAP, @@ -18,35 +17,6 @@ class PatientIcmr(PatientRegistration): class Meta: proxy = True - # @property - # def personal_details(self): - # return self - - # @property - # def specimen_details(self): - # instance = self.patientsample_set.last() - # if instance is not None: - # instance.__class__ = PatientSampleICMR - # return instance - - # @property - # def patient_category(self): - # instance = self.consultations.last() - # if instance: - # instance.__class__ = PatientConsultationICMR - # return instance - - # @property - # def exposure_history(self): - # return self - - # @property - # def medical_conditions(self): - # instance = self.patientsample_set.last() - # if instance is not None: - # instance.__class__ = PatientSampleICMR - # return instance - def get_age_delta(self): start = self.date_of_birth or timezone.datetime(self.year_of_birth, 1, 1).date() end = (self.death_datetime or timezone.now()).date() @@ -78,12 +48,14 @@ def state_name(self): @property def has_travel_to_foreign_last_14_days(self): + unsafe_travel_days = 14 if self.countries_travelled: return len(self.countries_travelled) != 0 and ( self.date_of_return - and (self.date_of_return.date() - datetime.datetime.now().date()).days - <= 14 + and (self.date_of_return.date() - now().date()).days + <= unsafe_travel_days ) + return None @property def travel_end_date(self): @@ -201,6 +173,7 @@ def symptoms(self): def date_of_onset_of_symptoms(self): if symptom := self.consultation.symptoms.first(): return symptom.onset_date.date() + return None class PatientConsultationICMR(PatientConsultation): @@ -208,26 +181,22 @@ class Meta: proxy = True def is_symptomatic(self): - if ( - SYMPTOM_CHOICES[0][0] not in self.symptoms.choices.keys() + return bool( + SYMPTOM_CHOICES[0][0] not in self.symptoms.choices or self.symptoms_onset_date is not None - ): - return True - else: - return False + ) def symptomatic_international_traveller( self, ): + unsafe_travel_days = 14 return bool( self.patient.countries_travelled and len(self.patient.countries_travelled) != 0 and ( self.patient.date_of_return - and ( - self.patient.date_of_return.date() - datetime.datetime.now().date() - ).days - <= 14 + and (self.patient.date_of_return.date() - now().date()).days + <= unsafe_travel_days ) and self.is_symptomatic() ) diff --git a/care/facility/models/shifting.py b/care/facility/models/shifting.py index 2a3dd20315..05d246a8f7 100644 --- a/care/facility/models/shifting.py +++ b/care/facility/models/shifting.py @@ -41,7 +41,9 @@ (40, "SEVERE"), ] -REVERSE_SHIFTING_STATUS_CHOICES = reverse_choices(SHIFTING_STATUS_CHOICES) +REVERSE_SHIFTING_STATUS_CHOICES: dict[int, str] = reverse_choices( + SHIFTING_STATUS_CHOICES +) class ShiftingRequest(FacilityBaseModel): diff --git a/care/facility/models/summary.py b/care/facility/models/summary.py index 5579bc564d..2cd1071bad 100644 --- a/care/facility/models/summary.py +++ b/care/facility/models/summary.py @@ -44,6 +44,9 @@ class Meta: models.Index(fields=["-created_date", "s_type"]), ] + def __str__(self): + return f"FacilityRelatedSummary - {self.facility} - {self.s_type}" + DISTRICT_SUMMARY_CHOICES = (("PatientSummary", "PatientSummary"),) @@ -78,6 +81,9 @@ class Meta: models.Index(fields=["-created_date", "s_type"]), ] + def __str__(self): + return f"DistrictScopedSummary - {self.district} - {self.s_type}" + LSG_SUMMARY_CHOICES = (("PatientSummary", "PatientSummary"),) @@ -109,3 +115,6 @@ class Meta: ), models.Index(fields=["-created_date", "s_type"]), ] + + def __str__(self): + return f"LocalBodyScopedSummary - {self.lsg} - {self.s_type}" diff --git a/care/utils/serializer/tests/__init__.py b/care/facility/models/tests/__init__.py similarity index 100% rename from care/utils/serializer/tests/__init__.py rename to care/facility/models/tests/__init__.py diff --git a/care/facility/signals/asset_updates.py b/care/facility/signals/asset_updates.py index 5d728e0482..2d6ba5c425 100644 --- a/care/facility/signals/asset_updates.py +++ b/care/facility/signals/asset_updates.py @@ -17,7 +17,7 @@ def save_asset_fields_before_update( return if instance.pk: - instance._previous_values = { + instance._previous_values = { # noqa: SLF001 "hostname": instance.resolved_middleware.get("hostname"), } diff --git a/care/facility/static_data/icd11.py b/care/facility/static_data/icd11.py index e591e99ae7..0861a1d2bc 100644 --- a/care/facility/static_data/icd11.py +++ b/care/facility/static_data/icd11.py @@ -1,3 +1,4 @@ +import logging import re from typing import TypedDict @@ -7,6 +8,9 @@ from care.facility.models.icd11_diagnosis import ICD11Diagnosis from care.utils.static_data.models.base import BaseRedisModel +logger = logging.getLogger(__name__) + + DISEASE_CODE_PATTERN = r"^(?:[A-Z]+\d|\d+[A-Z])[A-Z\d.]*\s" @@ -33,7 +37,7 @@ def get_representation(self) -> ICD11Object: def load_icd11_diagnosis(): - print("Loading ICD11 Diagnosis into the redis cache...", end="", flush=True) + logger.info("Loading ICD11 Diagnosis into the redis cache...") icd_objs = ICD11Diagnosis.objects.order_by("id").values_list( "id", "label", "meta_chapter_short" @@ -49,7 +53,7 @@ def load_icd11_diagnosis(): vec=diagnosis[1].replace(".", "\\.", 1), ).save() Migrator().run() - print("Done") + logger.info("ICD11 Diagnosis Loaded") def get_icd11_diagnosis_object_by_id( diff --git a/care/facility/static_data/medibase.py b/care/facility/static_data/medibase.py index 44f8935d18..03afab0e11 100644 --- a/care/facility/static_data/medibase.py +++ b/care/facility/static_data/medibase.py @@ -1,3 +1,4 @@ +import logging from typing import TypedDict from django.core.paginator import Paginator @@ -8,6 +9,8 @@ from care.facility.models.prescription import MedibaseMedicine as MedibaseMedicineModel from care.utils.static_data.models.base import BaseRedisModel +logger = logging.getLogger(__name__) + class MedibaseMedicineObject(TypedDict): id: str @@ -46,7 +49,7 @@ def get_representation(self) -> MedibaseMedicineObject: def load_medibase_medicines(): - print("Loading Medibase Medicines into the redis cache...", end="", flush=True) + logger.info("Loading Medibase Medicines into the redis cache...") medibase_objects = ( MedibaseMedicineModel.objects.order_by("external_id") @@ -87,4 +90,4 @@ def load_medibase_medicines(): vec=f"{medicine[1]} {medicine[3]} {medicine[4]}", ).save() Migrator().run() - print("Done") + logger.info("Medibase Medicines Loaded") diff --git a/care/facility/tasks/__init__.py b/care/facility/tasks/__init__.py index 6dd696c06e..f4e4774935 100644 --- a/care/facility/tasks/__init__.py +++ b/care/facility/tasks/__init__.py @@ -8,11 +8,11 @@ from care.facility.tasks.plausible_stats import capture_goals from care.facility.tasks.redis_index import load_redis_index from care.facility.tasks.summarisation import ( - summarise_district_patient, - summarise_facility_capacity, - summarise_patient, - summarise_tests, - summarise_triage, + summarize_district_patient, + summarize_facility_capacity, + summarize_patient, + summarize_tests, + summarize_triage, ) @@ -26,31 +26,31 @@ def setup_periodic_tasks(sender, **kwargs): if settings.TASK_SUMMARIZE_TRIAGE: sender.add_periodic_task( crontab(hour="*/4", minute="59"), - summarise_triage.s(), + summarize_triage.s(), name="summarise_triage", ) if settings.TASK_SUMMARIZE_TESTS: sender.add_periodic_task( crontab(hour="23", minute="59"), - summarise_tests.s(), + summarize_tests.s(), name="summarise_tests", ) if settings.TASK_SUMMARIZE_FACILITY_CAPACITY: sender.add_periodic_task( crontab(minute="*/5"), - summarise_facility_capacity.s(), + summarize_facility_capacity.s(), name="summarise_facility_capacity", ) if settings.TASK_SUMMARIZE_PATIENT: sender.add_periodic_task( crontab(hour="*/1", minute="59"), - summarise_patient.s(), + summarize_patient.s(), name="summarise_patient", ) if settings.TASK_SUMMARIZE_DISTRICT_PATIENT: sender.add_periodic_task( crontab(hour="*/1", minute="59"), - summarise_district_patient.s(), + summarize_district_patient.s(), name="summarise_district_patient", ) diff --git a/care/facility/tasks/asset_monitor.py b/care/facility/tasks/asset_monitor.py index a977bf1c7b..9c8701618c 100644 --- a/care/facility/tasks/asset_monitor.py +++ b/care/facility/tasks/asset_monitor.py @@ -1,6 +1,6 @@ import logging from datetime import datetime -from typing import Any +from typing import TYPE_CHECKING, Any from celery import shared_task from django.contrib.contenttypes.models import ContentType @@ -9,14 +9,16 @@ from care.facility.models.asset import Asset, AvailabilityRecord, AvailabilityStatus from care.utils.assetintegration.asset_classes import AssetClasses -from care.utils.assetintegration.base import BaseAssetIntegration + +if TYPE_CHECKING: + from care.utils.assetintegration.base import BaseAssetIntegration logger = logging.getLogger(__name__) @shared_task -def check_asset_status(): - logger.info(f"Checking Asset Status: {timezone.now()}") +def check_asset_status(): # noqa: PLR0912 + logger.info("Checking Asset Status: %s", timezone.now()) assets = ( Asset.objects.exclude(Q(asset_class=None) | Q(asset_class="")) @@ -50,7 +52,7 @@ def check_asset_status(): if not resolved_middleware: logger.warning( - f"Asset {asset.external_id} does not have a middleware hostname" + "Asset %s does not have a middleware hostname", asset.external_id ) continue @@ -91,7 +93,7 @@ def check_asset_status(): else: result = asset_class.api_get(asset_class.get_url("devices/status")) except Exception as e: - logger.warning(f"Middleware {resolved_middleware} is down", e) + logger.warning("Middleware %s is down: %s", resolved_middleware, e) # If no status is returned, setting default status as down if not result or "error" in result: @@ -138,4 +140,4 @@ def check_asset_status(): timestamp=status_record.get("time", timezone.now()), ) except Exception as e: - logger.error("Error in Asset Status Check", e) + logger.error("Error in Asset Status Check: %s", e) diff --git a/care/facility/tasks/discharge_summary.py b/care/facility/tasks/discharge_summary.py index bbcdc974c8..8f47a469c8 100644 --- a/care/facility/tasks/discharge_summary.py +++ b/care/facility/tasks/discharge_summary.py @@ -12,7 +12,7 @@ email_discharge_summary, generate_and_upload_discharge_summary, ) -from care.utils.exceptions import CeleryTaskException +from care.utils.exceptions import CeleryTaskError logger: Logger = get_task_logger(__name__) @@ -24,17 +24,17 @@ def generate_discharge_summary_task(consultation_ext_id: str): """ Generate and Upload the Discharge Summary """ - logger.info(f"Generating Discharge Summary for {consultation_ext_id}") + logger.info("Generating Discharge Summary for %s", consultation_ext_id) try: consultation = PatientConsultation.objects.get(external_id=consultation_ext_id) except PatientConsultation.DoesNotExist as e: - raise CeleryTaskException( - f"Consultation {consultation_ext_id} does not exist" - ) from e + msg = f"Consultation {consultation_ext_id} does not exist" + raise CeleryTaskError(msg) from e summary_file = generate_and_upload_discharge_summary(consultation) if not summary_file: - raise CeleryTaskException("Unable to generate discharge summary") + msg = "Unable to generate discharge summary" + raise CeleryTaskError(msg) return summary_file.id @@ -45,11 +45,11 @@ def generate_discharge_summary_task(consultation_ext_id: str): expires=10 * 60, ) def email_discharge_summary_task(file_id: int, emails: Iterable[str]): - logger.info(f"Emailing Discharge Summary {file_id} to {emails}") + logger.info("Emailing Discharge Summary %s to %s", file_id, emails) try: summary = FileUpload.objects.get(id=file_id) except FileUpload.DoesNotExist: - logger.error(f"Summary {file_id} does not exist") + logger.error("Summary %s does not exist", file_id) return False email_discharge_summary(summary, emails) return True diff --git a/care/facility/tasks/location_monitor.py b/care/facility/tasks/location_monitor.py index 6df3aef7d7..5c5b764b42 100644 --- a/care/facility/tasks/location_monitor.py +++ b/care/facility/tasks/location_monitor.py @@ -18,7 +18,7 @@ @shared_task def check_location_status(): location_content_type = ContentType.objects.get_for_model(AssetLocation) - logger.info(f"Checking Location Status: {timezone.now()}") + logger.info("Checking Location Status: %s", timezone.now()) locations = AssetLocation.objects.all() for location in locations: @@ -30,7 +30,8 @@ def check_location_status(): if not resolved_middleware: logger.warning( - f"No middleware hostname resolved for location {location.external_id}" + "No middleware hostname resolved for location %s", + location.external_id, ) continue @@ -54,7 +55,7 @@ def check_location_status(): new_status = AvailabilityStatus.OPERATIONAL except Exception as e: - logger.warning(f"Middleware {resolved_middleware} is down", e) + logger.warning("Middleware %s is down: %s", resolved_middleware, e) # Fetching the last record of the location last_record = ( @@ -74,6 +75,8 @@ def check_location_status(): status=new_status.value, timestamp=timezone.now(), ) - logger.info(f"Location {location.external_id} status: {new_status.value}") + logger.info( + "Location %s status: %s", location.external_id, new_status.value + ) except Exception as e: - logger.error("Error in Location Status Check", e) + logger.error("Error in Location Status Check: %s", e) diff --git a/care/facility/tasks/plausible_stats.py b/care/facility/tasks/plausible_stats.py index fdc897ebb4..c8f4660743 100644 --- a/care/facility/tasks/plausible_stats.py +++ b/care/facility/tasks/plausible_stats.py @@ -99,7 +99,7 @@ def capture_goals(): return today = now().date() yesterday = today - timedelta(days=1) - logger.info(f"Capturing Goals for {yesterday}") + logger.info("Capturing Goals for %s", yesterday) for goal in Goals: try: @@ -121,7 +121,7 @@ def capture_goals(): goal_entry_object.events = goal_data["results"]["events"]["value"] goal_entry_object.save() - logger.info(f"Saved goal entry for {goal_name} on {yesterday}") + logger.info("Saved goal entry for %s on %s", goal_name, yesterday) for property_name in goal.value: goal_property_stats = get_goal_event_stats( @@ -145,7 +145,11 @@ def capture_goals(): property_entry_object.events = property_statistic["events"] property_entry_object.save() logger.info( - f"Saved goal property entry for {goal_name} and property {property_name} on {yesterday}" + "Saved goal property entry for %s and property %s on %s", + goal_name, + property_name, + yesterday, ) + except Exception as e: - logger.error(f"Failed to process goal {goal_name} due to error: {e!s}") + logger.error("Failed to process goal %s due to error: %s", goal_name, e) diff --git a/care/facility/tasks/push_asset_config.py b/care/facility/tasks/push_asset_config.py index acccce370d..6baabe52b8 100644 --- a/care/facility/tasks/push_asset_config.py +++ b/care/facility/tasks/push_asset_config.py @@ -32,10 +32,10 @@ def create_asset_on_middleware(hostname: str, data: dict) -> dict: ) response.raise_for_status() response_json = response.json() - logger.info(f"Pushed Asset Configuration to Middleware: {response_json}") + logger.info("Pushed Asset Configuration to Middleware: %s", response_json) return response_json except Exception as e: - logger.error(f"Error Pushing Asset Configuration to Middleware: {e}") + logger.error("Error Pushing Asset Configuration to Middleware: %s", e) return {"error": str(e)} @@ -48,10 +48,10 @@ def delete_asset_from_middleware(hostname: str, asset_id: str) -> dict: ) response.raise_for_status() response_json = response.json() - logger.info(f"Deleted Asset from Middleware: {response_json}") + logger.info("Deleted Asset from Middleware: %s", response_json) return response_json except Exception as e: - logger.error(f"Error Deleting Asset from Middleware: {e}") + logger.error("Error Deleting Asset from Middleware: %s", e) return {"error": str(e)} @@ -68,10 +68,10 @@ def update_asset_on_middleware(hostname: str, asset_id: str, data: dict) -> dict ) response.raise_for_status() response_json = response.json() - logger.info(f"Updated Asset Configuration on Middleware: {response_json}") + logger.info("Updated Asset Configuration on Middleware: %s", response_json) return response_json except Exception as e: - logger.error(f"Error Updating Asset Configuration on Middleware: {e}") + logger.error("Error Updating Asset Configuration on Middleware: %s", e) return {"error": str(e)} diff --git a/care/facility/tasks/redis_index.py b/care/facility/tasks/redis_index.py index 0dba4797e7..306fc1352c 100644 --- a/care/facility/tasks/redis_index.py +++ b/care/facility/tasks/redis_index.py @@ -19,7 +19,7 @@ def load_redis_index(): logger.info("Redis Index already loading, skipping") return - cache.set("redis_index_loading", True, timeout=60 * 2) + cache.set("redis_index_loading", value=True, timeout=60 * 2) logger.info("Loading Redis Index") if index_exists(): logger.info("Index already exists, skipping") @@ -37,9 +37,9 @@ def load_redis_index(): if load_static_data: load_static_data() except ModuleNotFoundError: - logger.info(f"Module {module_path} not found") + logger.info("Module %s not found", module_path) except Exception as e: - logger.info(f"Error loading static data for {plug.name}: {e}") + logger.info("Error loading static data for %s: %s", plug.name, e) cache.delete("redis_index_loading") logger.info("Redis Index Loaded") diff --git a/care/facility/tasks/summarisation.py b/care/facility/tasks/summarisation.py index 774829df7c..541d032157 100644 --- a/care/facility/tasks/summarisation.py +++ b/care/facility/tasks/summarisation.py @@ -1,41 +1,44 @@ from celery import shared_task +from celery.utils.log import get_task_logger -from care.facility.utils.summarisation.district.patient_summary import ( +from care.facility.utils.summarization.district.patient_summary import ( district_patient_summary, ) -from care.facility.utils.summarisation.facility_capacity import ( +from care.facility.utils.summarization.facility_capacity import ( facility_capacity_summary, ) -from care.facility.utils.summarisation.patient_summary import patient_summary -from care.facility.utils.summarisation.tests_summary import tests_summary -from care.facility.utils.summarisation.triage_summary import triage_summary +from care.facility.utils.summarization.patient_summary import patient_summary +from care.facility.utils.summarization.tests_summary import tests_summary +from care.facility.utils.summarization.triage_summary import triage_summary + +logger = get_task_logger(__name__) @shared_task -def summarise_triage(): +def summarize_triage(): triage_summary() - print("Summarised Triages") + logger.info("Summarized Triages") @shared_task -def summarise_tests(): +def summarize_tests(): tests_summary() - print("Summarised Tests") + logger.info("Summarized Tests") @shared_task -def summarise_facility_capacity(): +def summarize_facility_capacity(): facility_capacity_summary() - print("Summarised Facility Capacities") + logger.info("Summarized Facility Capacities") @shared_task -def summarise_patient(): +def summarize_patient(): patient_summary() - print("Summarised Patients") + logger.info("Summarized Patients") @shared_task -def summarise_district_patient(): +def summarize_district_patient(): district_patient_summary() - print("Summarised District Patients") + logger.info("Summarized District Patients") diff --git a/care/facility/templatetags/__init__.py b/care/facility/templatetags/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/care/facility/templatetags/data_formatting_tags.py b/care/facility/templatetags/data_formatting_tags.py index a3e4403ce6..5e2a1ce087 100644 --- a/care/facility/templatetags/data_formatting_tags.py +++ b/care/facility/templatetags/data_formatting_tags.py @@ -5,7 +5,7 @@ @register.filter(name="format_empty_data") def format_empty_data(data): - if data is None or data == "" or data == 0.0 or data == []: + if data is None or data in ("", 0.0, []): return "N/A" return data @@ -28,7 +28,7 @@ def convert_to_sentence_case(s): converted_items = [convert_to_sentence_case(item) for item in items] return ", ".join(converted_items) - elif isinstance(data, (list, tuple)): + if isinstance(data, list | tuple): converted_items = [convert_to_sentence_case(item) for item in data] return ", ".join(converted_items) diff --git a/care/facility/templatetags/filters.py b/care/facility/templatetags/filters.py index 045819279c..2a15bc2d55 100644 --- a/care/facility/templatetags/filters.py +++ b/care/facility/templatetags/filters.py @@ -24,11 +24,12 @@ def suggestion_string(suggestion_code: str): def field_name_to_label(value): if value: return value.replace("_", " ").capitalize() + return None @register.filter(expects_localtime=True) def parse_datetime(value): try: - return datetime.strptime(value, "%Y-%m-%dT%H:%M") + return datetime.strptime(value, "%Y-%m-%dT%H:%M") # noqa: DTZ007 except ValueError: return None diff --git a/care/facility/templatetags/prescription_tags.py b/care/facility/templatetags/prescription_tags.py index 2f1b2ecee8..30d9f11a27 100644 --- a/care/facility/templatetags/prescription_tags.py +++ b/care/facility/templatetags/prescription_tags.py @@ -9,5 +9,4 @@ def format_prescription(prescription): return f"{prescription.medicine_name}, titration from {prescription.base_dosage} to {prescription.target_dosage}, {prescription.route}, {prescription.frequency} for {prescription.days} days." if prescription.dosage_type == "PRN": return f"{prescription.medicine_name}, {prescription.base_dosage}, {prescription.route}" - else: - return f"{prescription.medicine_name}, {prescription.base_dosage}, {prescription.route}, {prescription.frequency} for {prescription.days} days." + return f"{prescription.medicine_name}, {prescription.base_dosage}, {prescription.route}, {prescription.frequency} for {prescription.days} days." diff --git a/care/facility/tests/test_asset_public_api.py b/care/facility/tests/test_asset_public_api.py index 12b728c45e..40ccc77ce5 100644 --- a/care/facility/tests/test_asset_public_api.py +++ b/care/facility/tests/test_asset_public_api.py @@ -3,7 +3,7 @@ from rest_framework.test import APITestCase from care.facility.api.serializers.asset import AssetSerializer -from care.utils.tests.test_utils import TestUtils, override_cache +from care.utils.tests.test_utils import OverrideCache, TestUtils class AssetPublicViewSetTestCase(TestUtils, APITestCase): @@ -38,7 +38,7 @@ def test_retrieve_nonexistent_asset_qr_code(self): self.assertEqual(response.status_code, status.HTTP_404_NOT_FOUND) def test_retrieve_asset_qr_cached(self): - with override_cache(self): + with OverrideCache(self): response = self.client.get( f"/api/v1/public/asset_qr/{self.asset.qr_code_id}/" ) @@ -62,7 +62,7 @@ def test_retrieve_asset_qr_cached(self): self.assertEqual(response.data["name"], updated_data["name"]) def test_retrieve_asset_qr_pre_cached(self): - with override_cache(self): + with OverrideCache(self): serializer = AssetSerializer(self.asset) cache.set(f"asset:qr:{self.asset.qr_code_id}", serializer.data) response = self.client.get( diff --git a/care/facility/tests/test_asset_service_history_api.py b/care/facility/tests/test_asset_service_history_api.py index afa27962d1..d58dd01473 100644 --- a/care/facility/tests/test_asset_service_history_api.py +++ b/care/facility/tests/test_asset_service_history_api.py @@ -1,4 +1,4 @@ -from datetime import datetime, timedelta +from datetime import timedelta from django.utils.timezone import now from rest_framework import status @@ -20,8 +20,8 @@ def setUpTestData(cls): cls.asset_location = cls.create_asset_location(cls.facility) cls.asset = cls.create_asset(cls.asset_location) cls.user = cls.create_user("staff", cls.district, home_facility=cls.facility) - cls.today = datetime.today().strftime("%Y-%m-%d") - cls.yesterday = (datetime.today() - timedelta(days=1)).strftime("%Y-%m-%d") + cls.today = now().strftime("%Y-%m-%d") + cls.yesterday = (now() - timedelta(days=1)).strftime("%Y-%m-%d") cls.asset_service = AssetService.objects.create( asset=cls.asset, serviced_on=cls.today, diff --git a/care/facility/tests/test_medicine_administrations_api.py b/care/facility/tests/test_medicine_administrations_api.py index bbac0a5274..88fa18adbb 100644 --- a/care/facility/tests/test_medicine_administrations_api.py +++ b/care/facility/tests/test_medicine_administrations_api.py @@ -123,27 +123,21 @@ def test_administer_and_archive(self): f"/api/v1/consultation/{prescription.consultation.external_id}/prescription_administration/" ) self.assertEqual(res.status_code, status.HTTP_200_OK) - self.assertTrue( - any([administration_id == x["id"] for x in res.data["results"]]) - ) + self.assertTrue(any(administration_id == x["id"] for x in res.data["results"])) # test archived list administrations res = self.client.get( f"/api/v1/consultation/{prescription.consultation.external_id}/prescription_administration/?archived=true" ) self.assertEqual(res.status_code, status.HTTP_200_OK) - self.assertTrue( - any([administration_id == x["id"] for x in res.data["results"]]) - ) + self.assertTrue(any(administration_id == x["id"] for x in res.data["results"])) # test archived list administrations res = self.client.get( f"/api/v1/consultation/{prescription.consultation.external_id}/prescription_administration/?archived=false" ) self.assertEqual(res.status_code, status.HTTP_200_OK) - self.assertFalse( - any([administration_id == x["id"] for x in res.data["results"]]) - ) + self.assertFalse(any(administration_id == x["id"] for x in res.data["results"])) def test_administer_in_future(self): prescription = self.normal_prescription diff --git a/care/facility/tests/test_middleware_auth.py b/care/facility/tests/test_middleware_auth.py index bdb9453550..b7e5af1958 100644 --- a/care/facility/tests/test_middleware_auth.py +++ b/care/facility/tests/test_middleware_auth.py @@ -6,7 +6,7 @@ from rest_framework.test import APITestCase from care.utils.jwks.token_generator import generate_jwt -from care.utils.tests.test_utils import TestUtils, override_cache +from care.utils.tests.test_utils import OverrideCache, TestUtils class MiddlewareAuthTestCase(TestUtils, APITestCase): @@ -80,7 +80,7 @@ def test_middleware_authentication_successful(self, mock_get_public_key): response.data["username"], "middleware" + str(self.facility.external_id) ) - @override_cache + @OverrideCache @requests_mock.Mocker() def test_middleware_authentication_cached_successful(self, mock_get_public_key): mock_get_public_key.get( diff --git a/care/facility/tests/test_patient_and_consultation_access.py b/care/facility/tests/test_patient_and_consultation_access.py index b3decb5d63..8d5e9658f0 100644 --- a/care/facility/tests/test_patient_and_consultation_access.py +++ b/care/facility/tests/test_patient_and_consultation_access.py @@ -105,7 +105,7 @@ def test_discharge_patient_ordering_filter(self): self.assertEqual(str(patients_order[i].external_id), response[i]["id"]) # order by modified date - patients_order = patients_order[::-1] + patients_order.reverse() response = self.client.get( f"/api/v1/facility/{self.home_facility.external_id}/discharged_patients/?ordering=modified_date", ) @@ -113,7 +113,7 @@ def test_discharge_patient_ordering_filter(self): for i in range(len(response)): self.assertEqual(str(patients_order[i].external_id), response[i]["id"]) - def test_patient_consultation_access(self): + def test_patient_consultation_access(self): # noqa: PLR0915 # In this test, a patient is admitted to a remote facility and then later admitted to a home facility. # Admit patient to the remote facility diff --git a/care/facility/tests/test_patient_api.py b/care/facility/tests/test_patient_api.py index c40d57639b..01bff0b813 100644 --- a/care/facility/tests/test_patient_api.py +++ b/care/facility/tests/test_patient_api.py @@ -144,9 +144,9 @@ def create_patient_note( def test_patient_notes(self): self.client.force_authenticate(user=self.state_admin) - patientId = self.patient.external_id + patient_id = self.patient.external_id response = self.client.get( - f"/api/v1/patient/{patientId}/notes/", + f"/api/v1/patient/{patient_id}/notes/", { "consultation": self.consultation.external_id, "thread": PatientNoteThreadChoices.DOCTORS, @@ -160,7 +160,7 @@ def test_patient_notes(self): # Test if all notes are from same consultation as requested self.assertEqual( str(self.consultation.external_id), - [note["consultation"] for note in results][0], + next(note["consultation"] for note in results), ) # Test created_by_local_user field if user is not from same facility as patient @@ -278,13 +278,13 @@ def test_patient_note_with_reply(self): self.assertEqual(reply_response.status_code, status.HTTP_400_BAD_REQUEST) def test_patient_note_edit(self): - patientId = self.patient.external_id + patient_id = self.patient.external_id notes_list_response = self.client.get( - f"/api/v1/patient/{patientId}/notes/?consultation={self.consultation.external_id}" + f"/api/v1/patient/{patient_id}/notes/?consultation={self.consultation.external_id}" ) note_data = notes_list_response.json()["results"][0] response = self.client.get( - f"/api/v1/patient/{patientId}/notes/{note_data['id']}/edits/" + f"/api/v1/patient/{patient_id}/notes/{note_data['id']}/edits/" ) data = response.json()["results"] @@ -296,7 +296,7 @@ def test_patient_note_edit(self): # Test with a different user editing the note than the one who created it self.client.force_authenticate(user=self.state_admin) response = self.client.put( - f"/api/v1/patient/{patientId}/notes/{note_data['id']}/", + f"/api/v1/patient/{patient_id}/notes/{note_data['id']}/", {"note": new_note_content}, ) self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST) @@ -307,7 +307,7 @@ def test_patient_note_edit(self): # Test with the same user editing the note self.client.force_authenticate(user=self.user2) response = self.client.put( - f"/api/v1/patient/{patientId}/notes/{note_data['id']}/", + f"/api/v1/patient/{patient_id}/notes/{note_data['id']}/", {"note": new_note_content}, ) @@ -318,7 +318,7 @@ def test_patient_note_edit(self): # Ensure the original note is still present in the edits response = self.client.get( - f"/api/v1/patient/{patientId}/notes/{note_data['id']}/edits/" + f"/api/v1/patient/{patient_id}/notes/{note_data['id']}/edits/" ) data = response.json()["results"] @@ -402,16 +402,16 @@ def test_has_consent(self): response = self.client.get(self.get_base_url()) self.assertEqual(response.status_code, status.HTTP_200_OK) self.assertEqual(response.data["count"], 2) - patient_1_response = [ + patient_1_response = next( x for x in response.data["results"] if x["id"] == str(self.patient.external_id) - ][0] - patient_2_response = [ + ) + patient_2_response = next( x for x in response.data["results"] if x["id"] == str(self.patient_2.external_id) - ][0] + ) self.assertEqual( patient_1_response["last_consultation"]["has_consents"], [ConsentType.CONSENT_FOR_ADMISSION], @@ -424,11 +424,11 @@ def test_consent_edit(self): self.client.force_authenticate(user=self.user) response = self.client.get(self.get_base_url()) self.assertEqual(response.status_code, status.HTTP_200_OK) - patient_1_response = [ + patient_1_response = next( x for x in response.data["results"] if x["id"] == str(self.patient.external_id) - ][0] + ) self.assertEqual( patient_1_response["last_consultation"]["has_consents"], [ConsentType.CONSENT_FOR_ADMISSION], @@ -452,16 +452,16 @@ def test_has_consents_archived(self): response = self.client.get(self.get_base_url()) self.assertEqual(response.status_code, status.HTTP_200_OK) self.assertEqual(response.data["count"], 2) - patient_1_response = [ + patient_1_response = next( x for x in response.data["results"] if x["id"] == str(self.patient.external_id) - ][0] - patient_2_response = [ + ) + patient_2_response = next( x for x in response.data["results"] if x["id"] == str(self.patient_2.external_id) - ][0] + ) self.assertEqual( patient_1_response["last_consultation"]["has_consents"], [ConsentType.CONSENT_FOR_ADMISSION], @@ -477,16 +477,16 @@ def test_has_consents_archived(self): response = self.client.get(self.get_base_url()) self.assertEqual(response.status_code, status.HTTP_200_OK) self.assertEqual(response.data["count"], 2) - patient_1_response = [ + patient_1_response = next( x for x in response.data["results"] if x["id"] == str(self.patient.external_id) - ][0] - patient_2_response = [ + ) + patient_2_response = next( x for x in response.data["results"] if x["id"] == str(self.patient_2.external_id) - ][0] + ) self.assertEqual( patient_1_response["last_consultation"]["has_consents"], [ConsentType.CONSENT_FOR_ADMISSION], diff --git a/care/facility/tests/test_patient_consultation_api.py b/care/facility/tests/test_patient_consultation_api.py index 0bea4b9847..625206f6a8 100644 --- a/care/facility/tests/test_patient_consultation_api.py +++ b/care/facility/tests/test_patient_consultation_api.py @@ -64,7 +64,7 @@ def get_default_data(self): "onset_date": now(), }, ], - "patient_no": datetime.datetime.now().timestamp(), + "patient_no": now().timestamp(), } def get_url(self, consultation=None): diff --git a/care/facility/tests/test_patient_daily_rounds_api.py b/care/facility/tests/test_patient_daily_rounds_api.py index e636dfc37a..eb4a5cf9c5 100644 --- a/care/facility/tests/test_patient_daily_rounds_api.py +++ b/care/facility/tests/test_patient_daily_rounds_api.py @@ -1,4 +1,3 @@ -import datetime from datetime import timedelta from django.utils import timezone @@ -51,7 +50,7 @@ def setUpTestData(cls) -> None: "rounds_type": "NORMAL", "patient_category": "Comfort", "action": "DISCHARGE_RECOMMENDED", - "taken_at": datetime.datetime.now().isoformat(), + "taken_at": timezone.now().isoformat(), } def get_url(self, external_consultation_id=None): @@ -243,11 +242,6 @@ def test_create_pressure_sore_with_invalid_region(self): "region": "", "length": 1, "width": 1, - # "exudate_amount": "None", - # "tissue_type": "Closed", - # "push_score": 1, - # "scale": 1, - # "description": "Description", } ], ) diff --git a/care/facility/tests/test_pdf_generation.py b/care/facility/tests/test_pdf_generation.py index f049185aeb..8460b5c37b 100644 --- a/care/facility/tests/test_pdf_generation.py +++ b/care/facility/tests/test_pdf_generation.py @@ -1,4 +1,4 @@ -import os +import hashlib import subprocess import tempfile from datetime import date @@ -21,89 +21,65 @@ from care.utils.tests.test_utils import TestUtils -def compare_pngs(png_path1, png_path2): - with Image.open(png_path1) as img1, Image.open(png_path2) as img2: - if img1.mode != img2.mode: +def compare_images(image1_path: Path, image2_path: Path) -> bool: + with Image.open(image1_path) as img1, Image.open(image2_path) as img2: + if img1.mode != img2.mode or img1.size != img2.size: return False - if img1.size != img2.size: - return False + img1_hash = hashlib.sha256(img1.tobytes()).hexdigest() + img2_hash = hashlib.sha256(img2.tobytes()).hexdigest() - img1_data = list(img1.getdata()) - img2_data = list(img2.getdata()) + return img1_hash == img2_hash - if img1_data == img2_data: - return True - else: - return False +def test_compile_typ(data) -> bool: + logo_path = ( + Path(settings.BASE_DIR) / "staticfiles" / "images" / "logos" / "black-logo.svg" + ) + data["logo_path"] = str(logo_path) + content = render_to_string( + "reports/patient_discharge_summary_pdf_template.typ", context=data + ) + + sample_files_dir: Path = ( + settings.BASE_DIR / "care" / "facility" / "tests" / "sample_reports" + ) -def test_compile_typ(data): - sample_file_path = os.path.join( - os.getcwd(), "care", "facility", "tests", "sample_reports", "sample{n}.png" + subprocess.run( # noqa: S603 + [ # noqa: S607 + "typst", + "compile", + "-", + sample_files_dir / "test_output{n}.png", + "--format", + "png", + ], + input=content.encode("utf-8"), + capture_output=True, + check=True, + cwd="/", ) - test_output_file_path = os.path.join( - os.getcwd(), "care", "facility", "tests", "sample_reports", "test_output{n}.png" + + sample_files = sorted(sample_files_dir.glob("sample*.png")) + test_generated_files = sorted(sample_files_dir.glob("test_output*.png")) + + result = all( + compare_images(sample_image, test_output_image) + for sample_image, test_output_image in zip( + sample_files, test_generated_files, strict=True + ) ) - try: - logo_path = ( - Path(settings.BASE_DIR) - / "staticfiles" - / "images" - / "logos" - / "black-logo.svg" - ) - data["logo_path"] = str(logo_path) - content = render_to_string( - "reports/patient_discharge_summary_pdf_template.typ", context=data - ) - subprocess.run( - ["typst", "compile", "-", test_output_file_path, "--format", "png"], - input=content.encode("utf-8"), - capture_output=True, - check=True, - cwd="/", - ) - - number_of_pngs_generated = 2 - # To be updated only if the number of sample png increase in future - - for i in range(1, number_of_pngs_generated + 1): - current_sample_file_path = sample_file_path - current_sample_file_path = str(current_sample_file_path).replace( - "{n}", str(i) - ) - - current_test_output_file_path = test_output_file_path - current_test_output_file_path = str(current_test_output_file_path).replace( - "{n}", str(i) - ) - - if not compare_pngs( - Path(current_sample_file_path), Path(current_test_output_file_path) - ): - return False - return True - except Exception: - return False - finally: - count = 1 - while True: - current_test_output_file_path = test_output_file_path - current_test_output_file_path = current_test_output_file_path.replace( - "{n}", str(count) - ) - if Path(current_test_output_file_path).exists(): - os.remove(Path(current_test_output_file_path)) - else: - break - count += 1 + + for file in test_generated_files: + file.unlink() + + return result class TestTypstInstallation(TestCase): def test_typst_installed(self): try: - subprocess.run(["typst", "--version"], check=True, capture_output=True) + subprocess.run(["typst", "--version"], check=True, capture_output=True) # noqa: S603, S607 typst_installed = True except subprocess.CalledProcessError: typst_installed = False @@ -218,8 +194,8 @@ def test_pdf_generation_success(self): with tempfile.NamedTemporaryFile(suffix=".pdf", delete=False) as file: compile_typ(file.name, test_data) - self.assertTrue(os.path.exists(file.name)) - self.assertGreater(os.path.getsize(file.name), 0) + self.assertTrue(Path(file.name).exists()) + self.assertGreater(Path(file.name).stat().st_size, 0) def test_pdf_generation(self): data = discharge_summary.get_discharge_summary_data(self.consultation) diff --git a/care/facility/utils/icd/__init__.py b/care/facility/utils/icd/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/care/facility/utils/icd/scraper.py b/care/facility/utils/icd/scraper.py index 936ac5f4c2..a63c19213d 100644 --- a/care/facility/utils/icd/scraper.py +++ b/care/facility/utils/icd/scraper.py @@ -1,9 +1,17 @@ import json +import logging import time +from typing import TYPE_CHECKING import requests from django.conf import settings +if TYPE_CHECKING: + from pathlib import Path + + +logger = logging.getLogger(__name__) + class ICDScraper: def __init__(self): @@ -11,23 +19,22 @@ def __init__(self): self.child_concept_url = settings.ICD_SCRAPER_CHILD_CONCEPTS_URL self.scraped_concepts = [] self.scraped_concept_dict = {} + self.request_timeout = 10 - def add_query(self, url, query={}): - return ( - url - + "?" - + "&".join(map(lambda k: str(k) + "=" + str(query[k]), query.keys())) - ) + def add_query(self, url, query=None): + if query is None: + query = {} + return url + "?" + "&".join(str(k) + "=" + str(query[k]) for k in query) def get_child_concepts(self, p_concept, p_parent_id): if p_concept["ID"] in self.scraped_concept_dict: - print(f"[-] Skipped duplicate, {p_concept['label']}") + logger.info("[-] Skipped duplicate, %s", p_concept["label"]) return self.scraped_concepts.append({**p_concept, "parentId": p_parent_id}) self.scraped_concept_dict[p_concept["ID"]] = True - print(f"[+] Added {p_concept['label']}") + logger.info("[+] Added %s", p_concept["label"]) if p_concept["isLeaf"]: return @@ -41,12 +48,14 @@ def get_child_concepts(self, p_concept, p_parent_id): "useHtml": "false", "ConceptId": p_concept["ID"], }, - ) + ), + timeout=self.request_timeout, ).json() except Exception as e: - print("[x] Error encountered: ", e) - with open("error.txt", "a") as error_file: - error_file.write(f"{p_concept['label']}\n") + logger.info("[x] Error encountered: %s", e) + error_file: Path = settings.BASE_DIR / "error.txt" + with error_file.open("a") as ef: + ef.write(f"{p_concept['label']}\n") time.sleep(10) concepts = requests.get( @@ -56,7 +65,8 @@ def get_child_concepts(self, p_concept, p_parent_id): "useHtml": "false", "ConceptId": p_concept["ID"], }, - ) + ), + timeout=self.request_timeout, ).json() for concept in concepts: @@ -66,7 +76,8 @@ def scrape(self): self.scraped_concepts = [] self.scraped_concept_dict = {} root_concepts = requests.get( - self.add_query(self.root_concept_url, {"useHtml": "false"}) + self.add_query(self.root_concept_url, {"useHtml": "false"}), + timeout=self.request_timeout, ).json() skip = [ @@ -81,5 +92,6 @@ def scrape(self): self.get_child_concepts(root_concept, None) time.sleep(3) - with open("data.json", "w") as json_file: + data_file: Path = settings.BASE_DIR / "data" / "icd11.json" + with data_file.open("w") as json_file: json.dump(self.scraped_concepts, json_file) diff --git a/care/facility/utils/reports/__init__.py b/care/facility/utils/reports/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/care/facility/utils/reports/discharge_summary.py b/care/facility/utils/reports/discharge_summary.py index b31ca9762d..a66216f38d 100644 --- a/care/facility/utils/reports/discharge_summary.py +++ b/care/facility/utils/reports/discharge_summary.py @@ -112,7 +112,7 @@ def format_duration(duration): def get_discharge_summary_data(consultation: PatientConsultation): - logger.info(f"fetching discharge summary data for {consultation.external_id}") + logger.info("fetching discharge summary data for %s", consultation.external_id) samples = PatientSample.objects.filter( patient=consultation.patient, consultation=consultation ) @@ -212,8 +212,8 @@ def compile_typ(output_file, data): "reports/patient_discharge_summary_pdf_template.typ", context=data ) - subprocess.run( - [ + subprocess.run( # noqa: S603 + [ # noqa: S607 "typst", "compile", "-", @@ -226,29 +226,32 @@ def compile_typ(output_file, data): ) logging.info( - f"Successfully Compiled Summary pdf for {data['consultation'].external_id}" + "Successfully Compiled Summary pdf for %s", data["consultation"].external_id ) return True except subprocess.CalledProcessError as e: logging.error( - f"Error compiling summary pdf for {data['consultation'].external_id}: {e.stderr.decode('utf-8')}" + "Error compiling summary pdf for %s: %s", + data["consultation"].external_id, + e.stderr.decode("utf-8"), ) return False def generate_discharge_summary_pdf(data, file): logger.info( - f"Generating Discharge Summary pdf for {data['consultation'].external_id}" + "Generating Discharge Summary pdf for %s", data["consultation"].external_id ) compile_typ(output_file=file.name, data=data) logger.info( - f"Successfully Generated Discharge Summary pdf for {data['consultation'].external_id}" + "Successfully Generated Discharge Summary pdf for %s", + data["consultation"].external_id, ) def generate_and_upload_discharge_summary(consultation: PatientConsultation): - logger.info(f"Generating Discharge Summary for {consultation.external_id}") + logger.info("Generating Discharge Summary for %s", consultation.external_id) set_lock(consultation.external_id, 5) try: @@ -267,12 +270,14 @@ def generate_and_upload_discharge_summary(consultation: PatientConsultation): set_lock(consultation.external_id, 50) with tempfile.NamedTemporaryFile(suffix=".pdf") as file: generate_discharge_summary_pdf(data, file) - logger.info(f"Uploading Discharge Summary for {consultation.external_id}") + logger.info("Uploading Discharge Summary for %s", consultation.external_id) summary_file.put_object(file, ContentType="application/pdf") summary_file.upload_completed = True summary_file.save() logger.info( - f"Uploaded Discharge Summary for {consultation.external_id}, file id: {summary_file.id}" + "Uploaded Discharge Summary for %s, file id: %s", + consultation.external_id, + summary_file.id, ) finally: clear_lock(consultation.external_id) diff --git a/care/facility/utils/summarization/__init__.py b/care/facility/utils/summarization/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/care/facility/utils/summarization/district/__init__.py b/care/facility/utils/summarization/district/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/care/facility/utils/summarisation/district/patient_summary.py b/care/facility/utils/summarization/district/patient_summary.py similarity index 92% rename from care/facility/utils/summarisation/district/patient_summary.py rename to care/facility/utils/summarization/district/patient_summary.py index 31997109fb..a4f20a8d37 100644 --- a/care/facility/utils/summarisation/district/patient_summary.py +++ b/care/facility/utils/summarization/district/patient_summary.py @@ -43,9 +43,9 @@ def district_patient_summary(): home_quarantine = Q(last_consultation__suggestion="HI") total_patients_home_quarantine = patients.filter(home_quarantine).count() - district_summary[local_body_object.id][ - "total_patients_home_quarantine" - ] = total_patients_home_quarantine + district_summary[local_body_object.id]["total_patients_home_quarantine"] = ( + total_patients_home_quarantine + ) # Apply Date Filters @@ -69,9 +69,9 @@ def district_patient_summary(): district_summary[local_body_object.id][clean_name] = count # Update Anything Extra - district_summary[local_body_object.id][ - "today_patients_home_quarantine" - ] = today_patients_home_quarantine + district_summary[local_body_object.id]["today_patients_home_quarantine"] = ( + today_patients_home_quarantine + ) object_filter = Q(s_type="PatientSummary") & Q( created_date__startswith=now().date() diff --git a/care/facility/utils/summarisation/facility_capacity.py b/care/facility/utils/summarization/facility_capacity.py similarity index 85% rename from care/facility/utils/summarisation/facility_capacity.py rename to care/facility/utils/summarization/facility_capacity.py index a41ef71ec9..44df537cc6 100644 --- a/care/facility/utils/summarisation/facility_capacity.py +++ b/care/facility/utils/summarization/facility_capacity.py @@ -34,9 +34,9 @@ def facility_capacity_summary(): patients_in_facility.filter(is_active=True).count() ) discharge_patients = patients_in_facility.filter(is_active=False) - capacity_summary[facility_obj.id][ - "actual_discharged_patients" - ] = discharge_patients.count() + capacity_summary[facility_obj.id]["actual_discharged_patients"] = ( + discharge_patients.count() + ) capacity_summary[facility_obj.id]["availability"] = [] temp_inventory_summary_obj = {} @@ -53,14 +53,7 @@ def facility_capacity_summary(): created_date__gte=current_date, probable_accident=False, ) - # start_log = log_query.order_by("created_date").first() end_log = log_query.order_by("-created_date").first() - # start_stock = summary_obj.quantity_in_default_unit - # if start_log: - # if start_log.is_incoming: # Add current value to current stock to get correct stock - # start_stock = start_log.current_stock + start_log.quantity_in_default_unit - # else: - # start_stock = start_log.current_stock - start_log.quantity_in_default_unit end_stock = summary_obj.quantity if end_log: end_stock = end_log.current_stock @@ -77,11 +70,6 @@ def facility_capacity_summary(): if temp2: total_added = temp2.get("quantity_in_default_unit__sum", 0) or 0 - # Calculate Start Stock as - # end_stock = start_stock - consumption + addition - # start_stock = end_stock - addition + consumption - # This way the start stock will never veer off course - start_stock = end_stock - total_added + total_consumed if burn_rate: diff --git a/care/facility/utils/summarisation/patient_summary.py b/care/facility/utils/summarization/patient_summary.py similarity index 92% rename from care/facility/utils/summarisation/patient_summary.py rename to care/facility/utils/summarization/patient_summary.py index 3f3ca1a068..f54f79605b 100644 --- a/care/facility/utils/summarisation/patient_summary.py +++ b/care/facility/utils/summarization/patient_summary.py @@ -37,9 +37,9 @@ def patient_summary(): home_quarantine = Q(last_consultation__suggestion="HI") total_patients_home_quarantine = patients.filter(home_quarantine).count() - patient_summary[facility_id][ - "total_patients_home_quarantine" - ] = total_patients_home_quarantine + patient_summary[facility_id]["total_patients_home_quarantine"] = ( + total_patients_home_quarantine + ) # Apply Date Filters @@ -63,9 +63,9 @@ def patient_summary(): patient_summary[facility_id][clean_name] = count # Update Anything Extra - patient_summary[facility_id][ - "today_patients_home_quarantine" - ] = today_patients_home_quarantine + patient_summary[facility_id]["today_patients_home_quarantine"] = ( + today_patients_home_quarantine + ) for i in list(patient_summary.keys()): object_filter = Q(s_type="PatientSummary") & Q( diff --git a/care/facility/utils/summarisation/tests_summary.py b/care/facility/utils/summarization/tests_summary.py similarity index 100% rename from care/facility/utils/summarisation/tests_summary.py rename to care/facility/utils/summarization/tests_summary.py diff --git a/care/facility/utils/summarisation/triage_summary.py b/care/facility/utils/summarization/triage_summary.py similarity index 100% rename from care/facility/utils/summarisation/triage_summary.py rename to care/facility/utils/summarization/triage_summary.py diff --git a/care/users/admin.py b/care/users/admin.py index d8d066e931..ba40369025 100644 --- a/care/users/admin.py +++ b/care/users/admin.py @@ -53,7 +53,8 @@ class UserAdmin(auth_admin.UserAdmin, ExportCsvMixin): ) }, ), - ) + auth_admin.UserAdmin.fieldsets + *auth_admin.UserAdmin.fieldsets, + ) list_display = ["username", "is_superuser"] search_fields = ["first_name", "last_name"] @@ -89,7 +90,10 @@ class UserFlagForm(forms.ModelForm): ) class Meta: - fields = "__all__" + fields = ( + "user", + "flag", + ) model = UserFlag form = UserFlagForm diff --git a/care/users/api/serializers/user.py b/care/users/api/serializers/user.py index edb72c38b4..8d0211c206 100644 --- a/care/users/api/serializers/user.py +++ b/care/users/api/serializers/user.py @@ -18,8 +18,7 @@ custom_image_extension_validator, ) from care.utils.queryset.facility import get_home_facility_queryset -from care.utils.serializer.external_id_field import ExternalIdSerializerField -from config.serializers import ChoiceField +from care.utils.serializers.fields import ChoiceField, ExternalIdSerializerField class SignUpSerializer(serializers.ModelSerializer): @@ -88,6 +87,9 @@ def validate(self, attrs): return validated +MIN_USER_AGE = 16 + + class UserCreateSerializer(SignUpSerializer): password = serializers.CharField(required=False) facilities = serializers.ListSerializer( @@ -120,24 +122,24 @@ class Meta: date_of_birth = serializers.DateField(required=True) def validate_date_of_birth(self, value): - if value and now().year - value.year < 16: - raise serializers.ValidationError("Age must be greater than 15 years") + if value and now().year - value.year < MIN_USER_AGE: + error = "Age must be greater than 15 years" + raise serializers.ValidationError(error) return value def validate_facilities(self, facility_ids): - if facility_ids: - if ( - len(facility_ids) - != Facility.objects.filter(external_id__in=facility_ids).count() - ): - available_facility_ids = Facility.objects.filter( - external_id__in=facility_ids, - ).values_list("external_id", flat=True) - not_found_ids = list(set(facility_ids) - set(available_facility_ids)) - raise serializers.ValidationError( - f"Some facilities are not available - {', '.join([str(_id) for _id in not_found_ids])}", - ) + if ( + facility_ids + and len(facility_ids) + != Facility.objects.filter(external_id__in=facility_ids).count() + ): + available_facility_ids = Facility.objects.filter( + external_id__in=facility_ids, + ).values_list("external_id", flat=True) + not_found_ids = list(set(facility_ids) - set(available_facility_ids)) + error = f"Some facilities are not available - {', '.join([str(_id) for _id in not_found_ids])}" + raise serializers.ValidationError(error) return facility_ids def validate_ward(self, value): @@ -148,7 +150,8 @@ def validate_ward(self, value): and not self.context["created_by"].user_type >= User.TYPE_VALUE_MAP["LocalBodyAdmin"] ): - raise serializers.ValidationError("Cannot create for a different Ward") + error = "Cannot create for a different Ward" + raise serializers.ValidationError(error) return value def validate_local_body(self, value): @@ -159,9 +162,8 @@ def validate_local_body(self, value): and not self.context["created_by"].user_type >= User.TYPE_VALUE_MAP["DistrictAdmin"] ): - raise serializers.ValidationError( - "Cannot create for a different local body", - ) + error = "Cannot create for a different local body" + raise serializers.ValidationError(error) return value def validate_district(self, value): @@ -172,7 +174,8 @@ def validate_district(self, value): and not self.context["created_by"].user_type >= User.TYPE_VALUE_MAP["StateAdmin"] ): - raise serializers.ValidationError("Cannot create for a different district") + error = "Cannot create for a different district" + raise serializers.ValidationError(error) return value def validate_state(self, value): @@ -181,7 +184,8 @@ def validate_state(self, value): and value != self.context["created_by"].state and not self.context["created_by"].is_superuser ): - raise serializers.ValidationError("Cannot create for a different state") + error = "Cannot create for a different state" + raise serializers.ValidationError(error) return value def validate(self, attrs): @@ -195,15 +199,17 @@ def validate(self, attrs): }, ) - if self.context["created_by"].user_type in User.READ_ONLY_TYPES: - if validated["user_type"] not in User.READ_ONLY_TYPES: - raise exceptions.ValidationError( - { - "user_type": [ - "Read only users can create other read only users only", - ], - }, - ) + if ( + self.context["created_by"].user_type in User.READ_ONLY_TYPES + and validated["user_type"] not in User.READ_ONLY_TYPES + ): + raise exceptions.ValidationError( + { + "user_type": [ + "Read only users can create other read only users only", + ], + }, + ) if ( self.context["created_by"].user_type @@ -346,8 +352,9 @@ class Meta: extra_kwargs = {"url": {"lookup_field": "username"}} def validate_date_of_birth(self, value): - if value and now().year - value.year < 16: - raise serializers.ValidationError("Age must be greater than 15 years") + if value and now().year - value.year < MIN_USER_AGE: + error = "Age must be greater than 15 years" + raise serializers.ValidationError(error) return value diff --git a/care/users/api/serializers/userskill.py b/care/users/api/serializers/userskill.py index 0d75904700..20b504315d 100644 --- a/care/users/api/serializers/userskill.py +++ b/care/users/api/serializers/userskill.py @@ -2,7 +2,7 @@ from care.users.api.serializers.skill import SkillSerializer from care.users.models import Skill, UserSkill -from care.utils.serializer.external_id_field import ExternalIdSerializerField +from care.utils.serializers.fields import ExternalIdSerializerField class UserSkillSerializer(ModelSerializer): diff --git a/care/users/api/viewsets/users.py b/care/users/api/viewsets/users.py index 9deec755de..3f064d9d3a 100644 --- a/care/users/api/viewsets/users.py +++ b/care/users/api/viewsets/users.py @@ -76,9 +76,8 @@ def get_user_type( field_name, value, ): - if value: - if value in INVERSE_USER_TYPE: - return queryset.filter(user_type=INVERSE_USER_TYPE[value]) + if value and value in INVERSE_USER_TYPE: + return queryset.filter(user_type=INVERSE_USER_TYPE[value]) return queryset user_type = filters.CharFilter(method="get_user_type", field_name="user_type") @@ -123,21 +122,6 @@ class UserViewSet( filterset_class = UserFilterSet ordering_fields = ["id", "date_joined", "last_login"] search_fields = ["first_name", "last_name", "username"] - # last_login - # def get_permissions(self): - # return [ - # DRYPermissions(), - # IsAuthenticated(), - # ] - # if self.request.method == "POST": - # return [ - # DRYPermissions(), - # ] - # else: - # return [ - # IsAuthenticated(), - # DRYPermissions(), - # ] def get_queryset(self): if self.request.user.is_superuser: @@ -172,20 +156,18 @@ def get_queryset(self): def get_object(self) -> User: try: return super().get_object() - except Http404: - raise Http404("User not found") + except Http404 as e: + error = "User not found" + raise Http404(error) from e def get_serializer_class(self): if self.action == "list": return UserListSerializer - elif self.action == "add_user": + if self.action == "add_user": return UserCreateSerializer - # elif self.action == "create": - # return SignUpSerializer - elif self.action == "profile_picture": + if self.action == "profile_picture": return UserImageUploadSerializer - else: - return UserSerializer + return UserSerializer @extend_schema(tags=["users"]) @action(detail=False, methods=["GET"]) diff --git a/care/users/management/commands/load_data.py b/care/users/management/commands/load_data.py index 02a444fae5..53c6f70f4e 100644 --- a/care/users/management/commands/load_data.py +++ b/care/users/management/commands/load_data.py @@ -58,12 +58,11 @@ def handle(self, *args, **options): states = self.valid_states else: if state not in self.valid_states: - print("valid state options are ", self.valid_states) - raise Exception("State not found") + error = "State not found" + raise Exception(error) states = [state] for state in states: current_state_data = self.BASE_URL + state + "/lsg/" - print("Processing Files From", current_state_data) management.call_command("load_lsg_data", current_state_data) management.call_command("load_ward_data", current_state_data) diff --git a/care/users/management/commands/load_lsg_data.py b/care/users/management/commands/load_lsg_data.py index 4135a2c235..1398ce4f8c 100644 --- a/care/users/management/commands/load_lsg_data.py +++ b/care/users/management/commands/load_lsg_data.py @@ -1,6 +1,6 @@ -import glob import json from collections import defaultdict +from pathlib import Path from django.core.management.base import BaseCommand, CommandParser @@ -24,7 +24,7 @@ def handle(self, *args, **options) -> str | None: local_bodies = [] # Creates a map with first char of readable value as key - LOCAL_BODY_CHOICE_MAP = dict([(c[1][0], c[0]) for c in LOCAL_BODY_CHOICES]) + local_body_choice_map = {c[1][0]: c[0] for c in LOCAL_BODY_CHOICES} state = {} district = defaultdict(dict) @@ -34,7 +34,6 @@ def get_state_obj(state_name): return state[state_name] state_obj = State.objects.filter(name=state_name).first() if not state_obj: - print(f"Creating State {state_name}") state_obj = State(name=state_name) state_obj.save() state[state_name] = state_obj @@ -42,16 +41,14 @@ def get_state_obj(state_name): def get_district_obj(district_name, state_name): state_obj = get_state_obj(state_name) - if state_name in district: - if district_name in district[state_name]: - return district[state_name][district_name] + if state_name in district and district_name in district[state_name]: + return district[state_name][district_name] district_obj = District.objects.filter( name=district_name, state=state_obj ).first() if not district_obj: if not district_name: return None - print(f"Creating District {district_name}") district_obj = District(name=district_name, state=state_obj) district_obj.save() district[state_name][district_name] = district_obj @@ -80,7 +77,7 @@ def create_local_bodies(local_body_list): name=lb["name"], district=dist_obj, localbody_code=lb.get("localbody_code"), - body_type=LOCAL_BODY_CHOICE_MAP.get( + body_type=local_body_choice_map.get( (lb.get("localbody_code", " "))[0], LOCAL_BODY_CHOICES[-1][0], ), @@ -92,9 +89,8 @@ def create_local_bodies(local_body_list): # Hence, those records can be ignored using the `ignore_conflicts` flag LocalBody.objects.bulk_create(local_body_objs, ignore_conflicts=True) - for f in sorted(glob.glob(f"{folder}/*.json")): - counter += 1 - with open(f"{f}") as data_f: + for counter, f in enumerate(sorted(Path.glob(f"{folder}/*.json"))): + with Path(f).open() as data_f: data = json.load(data_f) data.pop("wards", None) local_bodies.append(data) @@ -103,7 +99,6 @@ def create_local_bodies(local_body_list): if counter % 1000 == 0: create_local_bodies(local_bodies) local_bodies = [] - print(f"Completed: {counter}") if len(local_bodies) > 0: create_local_bodies(local_bodies) diff --git a/care/users/management/commands/load_skill_data.py b/care/users/management/commands/load_skill_data.py index 3325ba448e..cf4d503734 100644 --- a/care/users/management/commands/load_skill_data.py +++ b/care/users/management/commands/load_skill_data.py @@ -10,7 +10,7 @@ class Command(BaseCommand): help = "Seed Data for Skills" - def handle(self, *args, **options): + def handle(self, *args, **kwargs): self.stdout.write("Seeding Skills Data... ", ending="") skills = [ diff --git a/care/users/management/commands/load_state_data.py b/care/users/management/commands/load_state_data.py index cf2895b915..969675e547 100644 --- a/care/users/management/commands/load_state_data.py +++ b/care/users/management/commands/load_state_data.py @@ -1,4 +1,5 @@ import json +from pathlib import Path from django.core.management import BaseCommand, CommandParser @@ -22,13 +23,12 @@ def handle(self, *args, **options): json_file_path = options["json_file_path"] data = [] - with open(json_file_path) as json_file: + with Path(json_file_path).open() as json_file: data = json.load(json_file) for item in data: state_name = item["state"].strip() if state_name.lower() in states_to_ignore: - print(f"Skipping {state_name}") continue districts = [d.strip() for d in item["districts"].split(",")] @@ -36,10 +36,8 @@ def handle(self, *args, **options): state, is_created = State.objects.get_or_create( name__iexact=state_name, defaults={"name": state_name} ) - print(f"{'Created' if is_created else 'Retrieved'} {state_name}") for d in districts: _, is_created = District.objects.get_or_create( state=state, name__iexact=d, defaults={"name": d} ) - print(f"{'Created' if is_created else 'Retrieved'} {state_name}") diff --git a/care/users/management/commands/load_ward_data.py b/care/users/management/commands/load_ward_data.py index c89cf47c8b..9b14dd2bba 100644 --- a/care/users/management/commands/load_ward_data.py +++ b/care/users/management/commands/load_ward_data.py @@ -1,5 +1,6 @@ -import glob import json +import logging +from pathlib import Path from django.core.management.base import BaseCommand, CommandParser from django.db import IntegrityError @@ -43,7 +44,7 @@ def get_ward_name(ward): district_map = {d.name: d for d in districts} # Creates a map with first char of readable value as key - LOCAL_BODY_CHOICE_MAP = dict([(c[1][0], c[0]) for c in LOCAL_BODY_CHOICES]) + local_body_choice_map = {c[1][0]: c[0] for c in LOCAL_BODY_CHOICES} def get_local_body(lb): if not lb["district"]: @@ -52,18 +53,18 @@ def get_local_body(lb): name=lb["name"], district=district_map[lb["district"]], localbody_code=lb.get("localbody_code"), - body_type=LOCAL_BODY_CHOICE_MAP.get( + body_type=local_body_choice_map.get( (lb.get("localbody_code", " "))[0], LOCAL_BODY_CHOICES[-1][0], ), ).first() - for f in sorted(glob.glob(f"{folder}/*.json")): - with open(f"{f}") as data_f: + for f in sorted(Path.glob(f"{folder}/*.json")): + with Path(f).open() as data_f: data = json.load(data_f) wards = data.pop("wards", None) if wards is None: - print("Ward Data not Found ") + logging.info("Ward Data not Found ") if data.get("district") is not None: local_body = get_local_body(data) if not local_body: @@ -80,4 +81,4 @@ def get_local_body(lb): obj.save() except IntegrityError: pass - print("Processed ", str(counter), " wards") + logging.info("Processed %s wards", str(counter)) diff --git a/care/users/management/commands/populate_investigations.py b/care/users/management/commands/populate_investigations.py index a802c66522..c64a46c1ae 100644 --- a/care/users/management/commands/populate_investigations.py +++ b/care/users/management/commands/populate_investigations.py @@ -1,4 +1,5 @@ import json +from pathlib import Path from django.core.management import BaseCommand from django.db import transaction @@ -8,10 +9,10 @@ PatientInvestigationGroup, ) -with open("data/investigations.json") as investigations_data: +with Path("data/investigations.json").open() as investigations_data: investigations = json.load(investigations_data) -with open("data/investigation_groups.json") as investigation_groups_data: +with Path("data/investigation_groups.json").open() as investigation_groups_data: investigation_groups = json.load(investigation_groups_data) @@ -22,7 +23,7 @@ class Command(BaseCommand): help = "Seed Data for Investigations" - def handle(self, *args, **options): + def handle(self, *args, **kwargs): investigation_group_dict = {} investigation_groups_to_create = [ diff --git a/care/users/management/commands/seed_data.py b/care/users/management/commands/seed_data.py index 0229caca9e..4bd59b3c02 100644 --- a/care/users/management/commands/seed_data.py +++ b/care/users/management/commands/seed_data.py @@ -15,9 +15,7 @@ class Command(BaseCommand): help = "Seed Data for Inventory" - def handle(self, *args, **options): - print("Creating Units for Inventory as well as their conversion rates") - + def handle(self, *args, **kwargs): # Inventory Unit items, _ = FacilityInventoryUnit.objects.get_or_create(name="Items") diff --git a/care/users/models.py b/care/users/models.py index 2a701a090e..6e8585878d 100644 --- a/care/users/models.py +++ b/care/users/models.py @@ -147,7 +147,7 @@ def create_superuser(self, username, email, password, **extra_fields): f"It looks like you haven't loaded district data. It is recommended to populate district data before you create a super user. Please run `python manage.py {data_command}`.\n Proceed anyway? [y/N]" ) if proceed.lower() != "y": - raise Exception("Aborted Superuser Creation") + raise Exception district = None extra_fields["district"] = district @@ -439,6 +439,9 @@ class UserFacilityAllocation(models.Model): start_date = models.DateTimeField(default=now) end_date = models.DateTimeField(null=True, blank=True) + def __str__(self): + return self.facility.name + class UserFlag(BaseFlag): user = models.ForeignKey(User, on_delete=models.CASCADE, null=False, blank=False) diff --git a/care/users/reset_password_views.py b/care/users/reset_password_views.py index b3bda5db83..89f67ae087 100644 --- a/care/users/reset_password_views.py +++ b/care/users/reset_password_views.py @@ -170,7 +170,7 @@ def post(self, request, *args, **kwargs): ) except ValidationError as e: # raise a validation error for the serializer - raise exceptions.ValidationError({"password": e.messages}) + raise exceptions.ValidationError({"password": e.messages}) from e reset_password_token.user.set_password(password) reset_password_token.user.save() diff --git a/care/users/signals.py b/care/users/signals.py index aba1418aff..f341ed3185 100644 --- a/care/users/signals.py +++ b/care/users/signals.py @@ -18,12 +18,6 @@ def password_reset_token_created( """ Handles password reset tokens When a token is created, an e-mail needs to be sent to the user - :param sender: View Class that sent the signal - :param instance: View Instance that sent the signal - :param reset_password_token: Token Model Object - :param args: - :param kwargs: - :return: """ # send an e-mail to the user context = { @@ -57,7 +51,7 @@ def save_fields_before_update(sender, instance, raw, using, update_fields, **kwa fields_to_save &= set(update_fields) if fields_to_save: with contextlib.suppress(IndexError): - instance._previous_values = instance.__class__._base_manager.filter( + instance._previous_values = instance.__class__._base_manager.filter( # noqa SLF001 pk=instance.pk ).values(*fields_to_save)[0] diff --git a/care/users/tests/test_models.py b/care/users/tests/test_models.py index e2f45d650b..4f06b9878a 100644 --- a/care/users/tests/test_models.py +++ b/care/users/tests/test_models.py @@ -15,12 +15,6 @@ def setUpTestData(cls): name="ghatak", description="corona virus specialist" ) - def test_max_length_name(self): - """Test max length for name is 255""" - skill = self.skill - max_length = skill._meta.get_field("name").max_length - self.assertEqual(max_length, 255) - def test_object_name(self): """Test that the name is returned while printing the object""" skill = self.skill @@ -35,12 +29,6 @@ def setUpTestData(cls): """ cls.state = State.objects.create(name="kerala") - def test_max_length_name(self): - """Test max length for name is 255""" - state = self.state - max_length = state._meta.get_field("name").max_length - self.assertEqual(max_length, 255) - def test_object_name(self): """Test that the correct format is returned while printing the object""" state = self.state @@ -56,12 +44,6 @@ def setUpTestData(cls): state = State.objects.create(name="uttar pradesh") cls.district = District.objects.create(state=state, name="name") - def test_max_length_name(self): - """Test max length for name is 255""" - district = self.district - max_length = district._meta.get_field("name").max_length - self.assertEqual(max_length, 255) - def test_object_name(self): """Test that the correct format is returned while printing the object""" district = self.district @@ -83,12 +65,6 @@ def setUpTestData(cls): district=district, name="blabla", body_type=1 ) - def test_max_length_name(self): - """Test max length for name is 255""" - local_body = self.local_body - max_length = local_body._meta.get_field("name").max_length - self.assertEqual(max_length, 255) - def test_object_name(self): """Test that the correct format is returned while printing the object""" local_body = self.local_body @@ -114,9 +90,3 @@ def setUpTestData(cls): gender=1, date_of_birth=date(2005, 1, 1), ) - - def test_max_length_phone_number(self): - """Test maximum length for phone number is 14""" - user = self.user - max_length = user._meta.get_field("phone_number").max_length - self.assertEqual(max_length, 14) diff --git a/care/utils/assetintegration/base.py b/care/utils/assetintegration/base.py index 3a430dcad0..334bcecfa5 100644 --- a/care/utils/assetintegration/base.py +++ b/care/utils/assetintegration/base.py @@ -2,6 +2,7 @@ import requests from django.conf import settings +from rest_framework import status from rest_framework.exceptions import APIException from care.utils.jwks.token_generator import generate_jwt @@ -33,40 +34,30 @@ def get_headers(self): "Accept": "application/json", } - def api_post(self, url, data=None): - req = requests.post( - url, - json=data, - headers=self.get_headers(), - timeout=self.timeout, - ) + def _validate_response(self, response: requests.Response): try: - response = req.json() - if req.status_code >= 400: - raise APIException(response, req.status_code) - return response + if response.status_code >= status.HTTP_400_BAD_REQUEST: + raise APIException(response.text, response.status_code) + return response.json() - except requests.Timeout: - raise APIException({"error": "Request Timeout"}, 504) + except requests.Timeout as e: + raise APIException({"error": "Request Timeout"}, 504) from e - except json.decoder.JSONDecodeError: - raise APIException({"error": "Invalid Response"}, req.status_code) + except json.decoder.JSONDecodeError as e: + raise APIException( + {"error": "Invalid Response"}, response.status_code + ) from e - def api_get(self, url, data=None): - req = requests.get( - url, - params=data, - headers=self.get_headers(), - timeout=self.timeout, + def api_post(self, url, data=None): + return self._validate_response( + requests.post( + url, json=data, headers=self.get_headers(), timeout=self.timeout + ) ) - try: - if req.status_code >= 400: - raise APIException(req.text, req.status_code) - response = req.json() - return response - - except requests.Timeout: - raise APIException({"error": "Request Timeout"}, 504) - except json.decoder.JSONDecodeError: - raise APIException({"error": "Invalid Response"}, req.status_code) + def api_get(self, url, data=None): + return self._validate_response( + requests.get( + url, params=data, headers=self.get_headers(), timeout=self.timeout + ) + ) diff --git a/care/utils/assetintegration/hl7monitor.py b/care/utils/assetintegration/hl7monitor.py index fffe61c963..abd14247d3 100644 --- a/care/utils/assetintegration/hl7monitor.py +++ b/care/utils/assetintegration/hl7monitor.py @@ -17,8 +17,8 @@ def __init__(self, meta): super().__init__(meta) except KeyError as e: raise ValidationError( - dict((key, f"{key} not found in asset metadata") for key in e.args) - ) + {key: f"{key} not found in asset metadata" for key in e.args} + ) from e def handle_action(self, action): action_type = action["type"] diff --git a/care/utils/assetintegration/onvif.py b/care/utils/assetintegration/onvif.py index 6b26053f0b..815994855e 100644 --- a/care/utils/assetintegration/onvif.py +++ b/care/utils/assetintegration/onvif.py @@ -24,8 +24,8 @@ def __init__(self, meta): self.access_key = self.meta["camera_access_key"].split(":")[2] except KeyError as e: raise ValidationError( - dict((key, f"{key} not found in asset metadata") for key in e.args) - ) + {key: f"{key} not found in asset metadata" for key in e.args} + ) from e def handle_action(self, action): action_type = action["type"] diff --git a/care/utils/assetintegration/ventilator.py b/care/utils/assetintegration/ventilator.py index a74ec0deb0..23a5280960 100644 --- a/care/utils/assetintegration/ventilator.py +++ b/care/utils/assetintegration/ventilator.py @@ -17,8 +17,8 @@ def __init__(self, meta): super().__init__(meta) except KeyError as e: raise ValidationError( - dict((key, f"{key} not found in asset metadata") for key in e.args) - ) + {key: f"{key} not found in asset metadata" for key in e.args} + ) from e def handle_action(self, action): action_type = action["type"] diff --git a/care/utils/cache/__init__.py b/care/utils/cache/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/care/utils/csp/__init__.py b/care/utils/csp/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/care/utils/csp/config.py b/care/utils/csp/config.py index edff720dc9..25b25c314a 100644 --- a/care/utils/csp/config.py +++ b/care/utils/csp/config.py @@ -1,5 +1,5 @@ import enum -from typing import TypeAlias, TypedDict +from typing import TypedDict from django.conf import settings @@ -11,7 +11,7 @@ class ClientConfig(TypedDict): endpoint_url: str -BucketName: TypeAlias = str +type BucketName = str class CSProvider(enum.Enum): @@ -57,6 +57,7 @@ def get_patient_bucket_config(external) -> tuple[ClientConfig, BucketName]: def get_client_config(bucket_type: BucketType, external=False): if bucket_type == BucketType.FACILITY: return get_facility_bucket_config(external=external) - elif bucket_type == BucketType.PATIENT: + if bucket_type == BucketType.PATIENT: return get_patient_bucket_config(external=external) - raise ValueError("Invalid Bucket Type") + msg = "Invalid Bucket Type" + raise ValueError(msg) diff --git a/care/utils/event_utils.py b/care/utils/event_utils.py index cb18c22128..02789c28f2 100644 --- a/care/utils/event_utils.py +++ b/care/utils/event_utils.py @@ -14,17 +14,17 @@ def is_null(data): def get_changed_fields(old: Model, new: Model) -> set[str]: changed_fields: set[str] = set() - for field in new._meta.fields: + for field in new._meta.fields: # noqa: SLF001 field_name = field.name if getattr(old, field_name, None) != getattr(new, field_name, None): changed_fields.add(field_name) return changed_fields -def serialize_field(object: Model, field_name: str): +def serialize_field(obj: Model, field_name: str): if "__" in field_name: field_name, sub_field = field_name.split("__", 1) - related_object = getattr(object, field_name, None) + related_object = getattr(obj, field_name, None) return serialize_field(related_object, sub_field) value = None @@ -33,13 +33,13 @@ def serialize_field(object: Model, field_name: str): except AttributeError: if object is not None: logger.warning( - f"Field {field_name} not found in {object.__class__.__name__}" + "Field %s not found in %s", field_name, object.__class__.__name__ ) return None try: # serialize choice fields with display value - field = object._meta.get_field(field_name) + field = object._meta.get_field(field_name) # noqa: SLF001 if issubclass(field.__class__, Field) and field.choices: value = getattr(object, f"get_{field_name}_display", lambda: value)() except FieldDoesNotExist: @@ -51,7 +51,7 @@ def serialize_field(object: Model, field_name: str): def model_diff(old, new): diff = {} - for field in new._meta.fields: + for field in new._meta.fields: # noqa: SLF001 field_name = field.name if getattr(old, field_name, None) != getattr(new, field_name, None): diff[field_name] = getattr(new, field_name, None) @@ -65,5 +65,5 @@ def default(self, o): return list(o) if isinstance(o, datetime): return o.isoformat() - logger.warning(f"Serializing Unknown Type {type(o)}, {o}") + logger.warning("Serializing Unknown Type %s, %s", type(o), o) return str(o) diff --git a/care/utils/exceptions.py b/care/utils/exceptions.py index de8c19ad8f..69609ddfd2 100644 --- a/care/utils/exceptions.py +++ b/care/utils/exceptions.py @@ -1,2 +1,2 @@ -class CeleryTaskException(Exception): +class CeleryTaskError(Exception): pass diff --git a/care/utils/file_uploads/cover_image.py b/care/utils/file_uploads/cover_image.py index 55f1183782..a774c451bc 100644 --- a/care/utils/file_uploads/cover_image.py +++ b/care/utils/file_uploads/cover_image.py @@ -18,14 +18,14 @@ def delete_cover_image(image_key: str, folder: Literal["cover_images", "avatars" try: s3.delete_object(Bucket=bucket_name, Key=image_key) except Exception: - logger.warning(f"Failed to delete cover image {image_key}") + logger.warning("Failed to delete cover image %s", image_key) def upload_cover_image( image: UploadedFile, object_external_id: str, folder: Literal["cover_images", "avatars"], - old_key: str = None, + old_key: str | None = None, ) -> str: config, bucket_name = get_client_config(BucketType.FACILITY) s3 = boto3.client("s3", **config) @@ -34,7 +34,7 @@ def upload_cover_image( try: s3.delete_object(Bucket=bucket_name, Key=old_key) except Exception: - logger.warning(f"Failed to delete old cover image {old_key}") + logger.warning("Failed to delete old cover image %s", old_key) image_extension = image.name.rsplit(".", 1)[-1] image_key = ( diff --git a/care/utils/filters/multiselect.py b/care/utils/filters/multiselect.py index fa4befe885..cf773127c4 100644 --- a/care/utils/filters/multiselect.py +++ b/care/utils/filters/multiselect.py @@ -9,5 +9,4 @@ def filter(self, qs, value): return None values_list = value.split(",") filters = {self.field_name + "__in": values_list} - qs = qs.filter(**filters) - return qs + return qs.filter(**filters) diff --git a/care/utils/jwks/generate_jwk.py b/care/utils/jwks/generate_jwk.py index b2e1565a43..5969ca2a20 100644 --- a/care/utils/jwks/generate_jwk.py +++ b/care/utils/jwks/generate_jwk.py @@ -17,10 +17,10 @@ def generate_encoded_jwks(): def get_jwks_from_file(base_path: Path): file_path = base_path / "jwks.b64.txt" try: - with open(file_path, "r") as file: + with file_path.open() as file: return file.read() except FileNotFoundError: jwks = generate_encoded_jwks() - with open(file_path, "w") as file: + with file_path.open("w") as file: file.write(jwks) return jwks diff --git a/care/utils/jwks/token_generator.py b/care/utils/jwks/token_generator.py index d6a169334b..0daee0f4f6 100644 --- a/care/utils/jwks/token_generator.py +++ b/care/utils/jwks/token_generator.py @@ -1,7 +1,6 @@ -from datetime import datetime - from authlib.jose import jwt from django.conf import settings +from django.utils.timezone import now def generate_jwt(claims=None, exp=60, jwks=None): @@ -10,7 +9,7 @@ def generate_jwt(claims=None, exp=60, jwks=None): if jwks is None: jwks = settings.JWKS header = {"alg": "RS256"} - time = int(datetime.now().timestamp()) + time = int(now().timestamp()) payload = { "iat": time, "exp": time + exp, diff --git a/care/utils/lock.py b/care/utils/lock.py index 333f8e5dc8..01576eedfd 100644 --- a/care/utils/lock.py +++ b/care/utils/lock.py @@ -15,7 +15,7 @@ def __init__(self, key, timeout=settings.LOCK_TIMEOUT): self.timeout = timeout def acquire(self): - if not cache.set(self.key, True, self.timeout, nx=True): + if not cache.set(self.key, value=True, timeout=self.timeout, nx=True): raise ObjectLocked def release(self): diff --git a/care/utils/models/base.py b/care/utils/models/base.py index f38fb5a501..62da9f4f50 100644 --- a/care/utils/models/base.py +++ b/care/utils/models/base.py @@ -11,12 +11,6 @@ def get_queryset(self): qs = super().get_queryset() return qs.filter(deleted=False) - # def filter(self, *args, **kwargs): - # _id = kwargs.pop("id", "----") - # if _id != "----" and not isinstance(_id, int): - # kwargs["external_id"] = _id - # return super().filter(*args, **kwargs) - class BaseModel(models.Model): external_id = models.UUIDField(default=uuid4, unique=True, db_index=True) @@ -52,6 +46,16 @@ class BaseFlag(BaseModel): class Meta: abstract = True + def save(self, *args, **kwargs): + self.validate_flag(self.flag) + cache.delete( + self.cache_key_template.format( + entity_id=self.entity_id, flag_name=self.flag + ) + ) + cache.delete(self.all_flags_cache_key_template.format(entity_id=self.entity_id)) + return super().save(*args, **kwargs) + @property def entity(self): return getattr(self, self.entity_field_name) @@ -64,16 +68,6 @@ def entity_id(self): def validate_flag(cls, flag_name: FlagName): FlagRegistry.validate_flag_name(cls.flag_type, flag_name) - def save(self, *args, **kwargs): - self.validate_flag(self.flag) - cache.delete( - self.cache_key_template.format( - entity_id=self.entity_id, flag_name=self.flag - ) - ) - cache.delete(self.all_flags_cache_key_template.format(entity_id=self.entity_id)) - return super().save(*args, **kwargs) - @classmethod def check_entity_has_flag(cls, entity_id: int, flag_name: FlagName) -> bool: cls.validate_flag(flag_name) diff --git a/care/utils/models/validators.py b/care/utils/models/validators.py index 1e50d905ea..03ff80fc51 100644 --- a/care/utils/models/validators.py +++ b/care/utils/models/validators.py @@ -48,6 +48,7 @@ def _extract_errors( message = str(error).replace("\n\n", ": ").replace("\n", "") container.append(ValidationError(message)) + return None @deconstructible @@ -98,13 +99,14 @@ class PhoneNumberValidator(RegexValidator): def __init__(self, types: Iterable[str], *args, **kwargs): if not isinstance(types, Iterable) or isinstance(types, str) or len(types) == 0: - raise ValueError("The `types` argument must be a non-empty iterable.") + msg = "The `types` argument must be a non-empty iterable." + raise ValueError(msg) self.types = types self.message = f"Invalid phone number. Must be one of the following types: {', '.join(self.types)}. Received: %(value)s" self.code = "invalid_phone_number" - self.regex = r"|".join([self.regex_map[type] for type in self.types]) + self.regex = r"|".join([self.regex_map[t] for t in self.types]) super().__init__(*args, **kwargs) def __eq__(self, other): @@ -139,39 +141,38 @@ def __init__( if not allow_floats and ( isinstance(min_amount, float) or isinstance(max_amount, float) ): - raise ValueError( + msg = ( "If floats are not allowed, min_amount and max_amount must be integers" ) + raise ValueError(msg) def __call__(self, value: str): try: amount, unit = value.split(" ", maxsplit=1) if unit not in self.allowed_units: - raise ValidationError( - f"Unit must be one of {', '.join(self.allowed_units)}" - ) + msg = f"Unit must be one of {', '.join(self.allowed_units)}" + raise ValidationError(msg) amount_number: int | float = float(amount) if amount_number.is_integer(): amount_number = int(amount_number) elif not self.allow_floats: - raise ValidationError("Input amount must be an integer") + msg = "Input amount must be an integer" + raise ValidationError(msg) elif len(str(amount_number).split(".")[1]) > self.precision: - raise ValidationError("Input amount must have at most 4 decimal places") + msg = "Input amount must have at most 4 decimal places" + raise ValidationError(msg) if len(amount) != len(str(amount_number)): - raise ValidationError( - f"Input amount must be a valid number without leading{' or trailing ' if self.allow_floats else ' '}zeroes" - ) + msg = f"Input amount must be a valid number without leading{' or trailing ' if self.allow_floats else ' '}zeroes" + raise ValidationError(msg) if self.min_amount > amount_number or amount_number > self.max_amount: - raise ValidationError( - f"Input amount must be between {self.min_amount} and {self.max_amount}" - ) - except ValueError: - raise ValidationError( - "Invalid Input, must be in the format: " - ) + msg = f"Input amount must be between {self.min_amount} and {self.max_amount}" + raise ValidationError(msg) + except ValueError as e: + msg = "Invalid Input, must be in the format: " + raise ValidationError(msg) from e def clean(self, value: str): if value is None: @@ -199,6 +200,16 @@ def __eq__(self, __value: object) -> bool: # pragma: no cover ) +class MiddlewareDomainAddressValidator(RegexValidator): + regex = r"^(?!https?:\/\/)[a-zA-Z0-9_-]+(\.[a-zA-Z0-9_-]+)*\.[a-zA-Z]{2,}$" + code = "invalid_domain_name" + message = _( + "The domain name is invalid. " + "It should not start with scheme and " + "should not end with a trailing slash." + ) + + @deconstructible class ImageSizeValidator: message: dict[str, str] = { @@ -242,9 +253,9 @@ def __init__( self.min_size = min_size self.max_size = max_size if aspect_ratio: - self.aspect_ratio = set( + self.aspect_ratio = { Fraction(ratio).limit_denominator(10) for ratio in aspect_ratio - ) + } self.aspect_ratio_str = ", ".join( f"{ratio.numerator}:{ratio.denominator}" for ratio in self.aspect_ratio ) @@ -317,10 +328,11 @@ def __eq__(self, other: object) -> bool: ) def _humanize_bytes(self, size: int) -> str: + byte_size = 1024.0 for unit in ["B", "KB"]: - if size < 1024.0: + if size < byte_size: return f"{f"{size:.2f}".rstrip(".0")} {unit}" - size /= 1024.0 + size /= byte_size return f"{f"{size:.2f}".rstrip(".0")} MB" diff --git a/care/utils/notification_handler.py b/care/utils/notification_handler.py index 97d0f38970..0ec1718ee0 100644 --- a/care/utils/notification_handler.py +++ b/care/utils/notification_handler.py @@ -1,4 +1,5 @@ import json +import logging from celery import shared_task from django.apps import apps @@ -16,10 +17,12 @@ ) from care.facility.models.shifting import ShiftingRequest from care.users.models import User -from care.utils.sms.sendSMS import sendSMS +from care.utils.sms.send_sms import send_sms +logger = logging.getLogger(__name__) -class NotificationCreationException(Exception): + +class NotificationCreationError(Exception): pass @@ -41,6 +44,7 @@ def get_model_class(model_name): return apps.get_model(f"facility.{model_name}") +# ruff: noqa: SIM102, PLR0912 rebuilding the notification generator would be easier class NotificationGenerator: generate_for_facility = False generate_for_user = False @@ -64,23 +68,23 @@ def __init__( ): if not worker_initated: if not isinstance(event_type, Notification.EventType): - raise NotificationCreationException("Event Type Invalid") + msg = "Event Type Invalid" + raise NotificationCreationError(msg) if not isinstance(event, Notification.Event): - raise NotificationCreationException("Event Invalid") + msg = "Event Invalid" + raise NotificationCreationError(msg) if not isinstance(caused_by, User): - raise NotificationCreationException( - "edited_by must be an instance of a user" - ) - if facility: - if not isinstance(facility, Facility): - raise NotificationCreationException( - "facility must be an instance of Facility" - ) + msg = "edited_by must be an instance of a user" + raise NotificationCreationError(msg) + if facility and not isinstance(facility, Facility): + msg = "facility must be an instance of Facility" + raise NotificationCreationError(msg) mediums = [] if notification_mediums: for medium in notification_mediums: if not isinstance(medium, Notification.Medium): - raise NotificationCreationException("Medium Type Invalid") + msg = "Medium Type Invalid" + raise NotificationCreationError(msg) mediums.append(medium.value) data = { "event_type": event_type.value, @@ -102,8 +106,7 @@ def __init__( self.worker_initiated = False return self.worker_initiated = True - Model = get_model_class(caused_object) - caused_object = Model.objects.get(id=caused_object_pk) + caused_object = get_model_class(caused_object).objects.get(id=caused_object_pk) caused_by = User.objects.get(id=caused_by) facility = Facility.objects.get(id=facility) self.notification_mediums = notification_mediums @@ -231,6 +234,7 @@ def generate_sms_phone_numbers(self): self.caused_object.patient.phone_number, self.caused_object.patient.emergency_phone_number, ] + return None def _get_default_medium(self): return [Notification.Medium.SYSTEM.value] @@ -347,17 +351,17 @@ def send_webpush_user(self, user, message): }, ) except WebPushException as ex: - print("Web Push Failed with Exception: {}", repr(ex)) + logger.info("Web Push Failed with Exception: %s", repr(ex)) if ex.response and ex.response.json(): extra = ex.response.json() - print( - "Remote service replied with a {}:{}, {}", + logger.info( + "Remote service replied with a %s:%s, %s", extra.code, extra.errno, extra.message, ) except Exception as e: - print("Error When Doing WebPush", e) + logger.info("Error When Doing WebPush: %s", e) def generate(self): if not self.worker_initiated: @@ -367,7 +371,7 @@ def generate(self): medium == Notification.Medium.SMS.value and settings.SEND_SMS_NOTIFICATION ): - sendSMS( + send_sms( self.generate_sms_phone_numbers(), self.generate_sms_message(), many=True, diff --git a/care/utils/queryset/__init__.py b/care/utils/queryset/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/care/utils/registries/feature_flag.py b/care/utils/registries/feature_flag.py index dba42888e5..a1f1406f98 100644 --- a/care/utils/registries/feature_flag.py +++ b/care/utils/registries/feature_flag.py @@ -1,13 +1,12 @@ import enum import logging -from typing import TypeAlias from django.core.exceptions import ValidationError logger = logging.getLogger(__name__) -class FlagNotFoundException(ValidationError): +class FlagNotFoundError(ValidationError): pass @@ -16,9 +15,8 @@ class FlagType(enum.Enum): FACILITY = "FACILITY" -# TODO: convert to type in python 3.12 -FlagName = str -FlagTypeRegistry: TypeAlias = dict[FlagType, dict[FlagName, bool]] +type FlagName = str +type FlagTypeRegistry = dict[FlagType, dict[FlagName, bool]] class FlagRegistry: @@ -35,7 +33,7 @@ def unregister(cls, flag_type, flag_name) -> None: try: del cls._flags[flag_type][flag_name] except KeyError as e: - logger.warning(f"Flag {flag_name} not found in {flag_type}: {e}") + logger.warning("Flag %s not found in %s: %s", flag_name, flag_type, e) @classmethod def register_wrapper(cls, flag_type, flag_name) -> None: @@ -48,13 +46,15 @@ def inner_wrapper(wrapped_class): @classmethod def validate_flag_type(cls, flag_type: FlagType) -> None: if flag_type not in cls._flags: - raise FlagNotFoundException("Invalid Flag Type") + msg = "Invalid Flag Type" + raise FlagNotFoundError(msg) @classmethod def validate_flag_name(cls, flag_type: FlagType, flag_name): cls.validate_flag_type(flag_type) if flag_name not in cls._flags[flag_type]: - raise FlagNotFoundException("Flag not registered") + msg = "Flag not registered" + raise FlagNotFoundError(msg) @classmethod def get_all_flags(cls, flag_type: FlagType) -> list[FlagName]: @@ -65,4 +65,4 @@ def get_all_flags(cls, flag_type: FlagType) -> list[FlagName]: def get_all_flags_as_choices( cls, flag_type: FlagType ) -> list[tuple[FlagName, FlagName]]: - return ((x, x) for x in cls._flags.get(flag_type, {}).keys()) + return ((x, x) for x in cls._flags.get(flag_type, {})) diff --git a/care/utils/serializers/__init__.py b/care/utils/serializers/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/care/utils/serializer/external_id_field.py b/care/utils/serializers/fields.py similarity index 52% rename from care/utils/serializer/external_id_field.py rename to care/utils/serializers/fields.py index 43619d4334..f6ae57bf0d 100644 --- a/care/utils/serializer/external_id_field.py +++ b/care/utils/serializers/fields.py @@ -9,8 +9,9 @@ class UUIDValidator: def __call__(self, value): try: return uuid.UUID(value) - except ValueError: - raise serializers.ValidationError("invalid uuid") + except ValueError as e: + msg = "invalid uuid" + raise serializers.ValidationError(msg) from e class ExternalIdSerializerField(serializers.UUIDField): @@ -34,6 +35,23 @@ def run_validation(self, data=empty): if value: try: value = self.queryset.get(external_id=value) - except ObjectDoesNotExist: - raise serializers.ValidationError("object with this id not found") + except ObjectDoesNotExist as e: + msg = "object with this id not found" + raise serializers.ValidationError(msg) from e return value + + +class ChoiceField(serializers.ChoiceField): + def to_representation(self, obj): + try: + return self._choices[obj] + except KeyError: + key_type = type(next(iter(self.choices.keys()))) + key = key_type(obj) + return self._choices[key] + + def to_internal_value(self, data): + if isinstance(data, str) and data not in self.choice_strings_to_values: + choice_name_map = {v: k for k, v in self._choices.items()} + data = choice_name_map.get(data) + return super().to_internal_value(data) diff --git a/care/utils/serializer/history_serializer.py b/care/utils/serializers/history_serializer.py similarity index 100% rename from care/utils/serializer/history_serializer.py rename to care/utils/serializers/history_serializer.py diff --git a/care/utils/sms/__init__.py b/care/utils/sms/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/care/utils/sms/sendSMS.py b/care/utils/sms/send_sms.py similarity index 76% rename from care/utils/sms/sendSMS.py rename to care/utils/sms/send_sms.py index dbf0fc9edd..38b4816895 100644 --- a/care/utils/sms/sendSMS.py +++ b/care/utils/sms/send_sms.py @@ -1,10 +1,14 @@ +import logging + import boto3 from django.conf import settings from care.utils.models.validators import mobile_validator +logger = logging.getLogger(__name__) + -def sendSMS(phone_numbers, message, many=False): +def send_sms(phone_numbers, message, many=False): if not many: phone_numbers = [phone_numbers] phone_numbers = list(set(phone_numbers)) @@ -12,6 +16,8 @@ def sendSMS(phone_numbers, message, many=False): try: mobile_validator(phone) except Exception: + if settings.DEBUG: + logger.error("Invalid Phone Number %s", phone) continue client = boto3.client( "sns", diff --git a/care/utils/tests/test_feature_flags.py b/care/utils/tests/test_feature_flags.py index 13324f20f6..6afda21dbb 100644 --- a/care/utils/tests/test_feature_flags.py +++ b/care/utils/tests/test_feature_flags.py @@ -1,12 +1,13 @@ from django.test import TestCase from care.utils.registries.feature_flag import ( - FlagNotFoundException, + FlagNotFoundError, FlagRegistry, FlagType, ) +# ruff: noqa: SLF001 class FeatureFlagTestCase(TestCase): def setUp(self): FlagRegistry._flags = {} @@ -33,7 +34,7 @@ def test_validate_flag_type(self): self.assertIsNone(FlagRegistry.validate_flag_type(FlagType.USER)) def test_validate_flag_type_invalid(self): - with self.assertRaises(FlagNotFoundException): + with self.assertRaises(FlagNotFoundError): FlagRegistry.validate_flag_type( FlagType.USER ) # FlagType.USER is not registered @@ -43,12 +44,12 @@ def test_validate_flag_name(self): self.assertIsNone(FlagRegistry.validate_flag_name(FlagType.USER, "TEST_FLAG")) def test_validate_flag_name_invalid(self): - with self.assertRaises(FlagNotFoundException) as ectx: + with self.assertRaises(FlagNotFoundError) as ectx: FlagRegistry.validate_flag_name(FlagType.USER, "TEST_OTHER_FLAG") self.assertEqual(ectx.exception.message, "Invalid Flag Type") FlagRegistry.register(FlagType.USER, "TEST_FLAG") - with self.assertRaises(FlagNotFoundException) as ectx: + with self.assertRaises(FlagNotFoundError) as ectx: FlagRegistry.validate_flag_name(FlagType.USER, "TEST_OTHER_FLAG") self.assertEqual(ectx.exception.message, "Flag not registered") diff --git a/care/utils/tests/test_utils.py b/care/utils/tests/test_utils.py index 707f8b8308..1f858c7258 100644 --- a/care/utils/tests/test_utils.py +++ b/care/utils/tests/test_utils.py @@ -56,7 +56,7 @@ fake = Faker() -class override_cache(override_settings): +class OverrideCache(override_settings): """ Overrides the cache settings for the test to use a local memory cache instead of the redis cache @@ -85,16 +85,19 @@ def __eq__(self, other): mock_equal = EverythingEquals() -def assert_equal_dicts(d1, d2, ignore_keys=[]): +def assert_equal_dicts(d1, d2, ignore_keys=None): + if ignore_keys is None: + ignore_keys = [] + def check_equal(): ignored = set(ignore_keys) for k1, v1 in d1.items(): if k1 not in ignored and (k1 not in d2 or d2[k1] != v1): - print(k1, v1, d2[k1]) + print(k1, v1, d2[k1]) # noqa: T201 return False for k2, v2 in d2.items(): if k2 not in ignored and k2 not in d1: - print(k2, v2) + print(k2, v2) # noqa: T201 return False return True @@ -142,7 +145,7 @@ def create_local_body(cls, district: District, **kwargs) -> LocalBody: return LocalBody.objects.create(**data) @classmethod - def get_user_data(cls, district: District, user_type: str = None): + def get_user_data(cls, district: District, user_type: str | None = None): """ Returns the data to be used for API testing @@ -174,7 +177,7 @@ def link_user_with_facility(cls, user: User, facility: Facility, created_by: Use @classmethod def create_user( cls, - username: str = None, + username: str | None = None, district: District = None, local_body: LocalBody = None, **kwargs, @@ -265,8 +268,7 @@ def create_facility( "created_by": user, } data.update(kwargs) - facility = Facility.objects.create(**data) - return facility + return Facility.objects.create(**data) @classmethod def get_patient_data(cls, district, state) -> dict: @@ -348,7 +350,7 @@ def get_consultation_data(cls) -> dict: "discharge_date": None, "consultation_notes": "", "course_in_facility": "", - "patient_no": int(datetime.now().timestamp() * 1000), + "patient_no": int(now().timestamp() * 1000), "route_to_facility": 10, } @@ -492,7 +494,7 @@ def create_patient_consent( @classmethod def clone_object(cls, obj, save=True): - new_obj = obj._meta.model.objects.get(pk=obj.id) + new_obj = obj._meta.model.objects.get(pk=obj.id) # noqa: SLF001 new_obj.pk = None new_obj.id = None try: @@ -625,8 +627,7 @@ def get_patient_investigation_group_data(cls) -> dict: def create_patient_investigation_group(cls, **kwargs) -> PatientInvestigationGroup: data = cls.get_patient_investigation_group_data() data.update(**kwargs) - investigation_group = PatientInvestigationGroup.objects.create(**data) - return investigation_group + return PatientInvestigationGroup.objects.create(**data) @classmethod def get_patient_investigation_session_data(cls, user) -> dict: @@ -640,8 +641,7 @@ def create_patient_investigation_session( ) -> InvestigationSession: data = cls.get_patient_investigation_session_data(user) data.update(**kwargs) - investigation_session = InvestigationSession.objects.create(**data) - return investigation_session + return InvestigationSession.objects.create(**data) @classmethod def get_investigation_value_data( @@ -694,7 +694,7 @@ def get_prescription_data(cls, consultation, user) -> dict: return { "consultation": consultation, "prescription_type": "REGULAR", - "medicine": None, # TODO : Create medibase medicine + "medicine": None, "medicine_old": "Sample old Medicine", "route": "Oral", "base_dosage": "500mg", @@ -761,17 +761,16 @@ def get_local_body_district_state_representation(self, obj): def get_local_body_representation(self, local_body: LocalBody): if local_body is None: return {"local_body": None, "local_body_object": None} - else: - return { - "local_body": local_body.id, - "local_body_object": { - "id": local_body.id, - "name": local_body.name, - "district": local_body.district.id, - "localbody_code": local_body.localbody_code, - "body_type": local_body.body_type, - }, - } + return { + "local_body": local_body.id, + "local_body_object": { + "id": local_body.id, + "name": local_body.name, + "district": local_body.district.id, + "localbody_code": local_body.localbody_code, + "body_type": local_body.body_type, + }, + } def get_district_representation(self, district: District): if district is None: @@ -795,16 +794,16 @@ def dict_to_matching_type(d: dict): return {k: to_matching_type(k, v) for k, v in d.items()} def to_matching_type(name: str, value): - if isinstance(value, (OrderedDict, dict)): + if isinstance(value, OrderedDict | dict): return dict_to_matching_type(dict(value)) - elif isinstance(value, list): + if isinstance(value, list): return [to_matching_type("", v) for v in value] - elif "date" in name and not isinstance( - value, (type(None), EverythingEquals) - ): + if "date" in name and not isinstance(value, type(None) | EverythingEquals): return_value = value if isinstance(value, str): - return_value = datetime.strptime(value, "%Y-%m-%dT%H:%M:%S.%fZ") + return_value = datetime.strptime( + value, "%Y-%m-%dT%H:%M:%S.%fZ" + ).astimezone() return ( return_value.astimezone(tz=UTC) if isinstance(return_value, datetime) @@ -824,16 +823,15 @@ def execute_list(self, user=None): def get_facility_representation(self, facility): if facility is None: return facility - else: - return { - "id": str(facility.external_id), - "name": facility.name, - "facility_type": { - "id": facility.facility_type, - "name": facility.get_facility_type_display(), - }, - **self.get_local_body_district_state_representation(facility), - } + return { + "id": str(facility.external_id), + "name": facility.name, + "facility_type": { + "id": facility.facility_type, + "name": facility.get_facility_type_display(), + }, + **self.get_local_body_district_state_representation(facility), + } def create_patient_note( self, patient=None, note="Patient is doing find", created_by=None, **kwargs @@ -843,14 +841,14 @@ def create_patient_note( "note": note, } data.update(kwargs) - patientId = patient.external_id + patient_id = patient.external_id refresh_token = RefreshToken.for_user(created_by) self.client.credentials( HTTP_AUTHORIZATION=f"Bearer {refresh_token.access_token}" ) - self.client.post(f"/api/v1/patient/{patientId}/notes/", data=data) + self.client.post(f"/api/v1/patient/{patient_id}/notes/", data=data) @classmethod def create_patient_shift( diff --git a/care/utils/ulid/models.py b/care/utils/ulid/models.py index 9305735e30..bc45cf1c26 100644 --- a/care/utils/ulid/models.py +++ b/care/utils/ulid/models.py @@ -35,9 +35,9 @@ def to_python(self, value) -> ULID | None: return None try: return ULID.parse(value) - except (AttributeError, ValueError): + except (AttributeError, ValueError) as e: raise exceptions.ValidationError( self.error_messages["invalid"], code="invalid", params={"value": value}, - ) + ) from e diff --git a/care/utils/ulid/ulid.py b/care/utils/ulid/ulid.py index f697c60041..c718b88031 100644 --- a/care/utils/ulid/ulid.py +++ b/care/utils/ulid/ulid.py @@ -1,35 +1,63 @@ from typing import Self from uuid import UUID -from ulid import ULID as BaseULID +from ulid import ULID as BaseULID # noqa: N811 + +UUID_LEN_WITHOUT_HYPHENS = 32 +UUID_LEN_WITH_HYPHENS = 36 +ULID_STR_LEN = 26 +ULID_BYTES_LEN = 16 +TIMESTAMP_STR_LEN = 10 class ULID(BaseULID): @classmethod def parse(cls, value) -> Self: if isinstance(value, BaseULID): - return value + return cls.parse_baseulid(value) if isinstance(value, UUID): - return cls.from_uuid(value) + return cls.parse_uuid(value) if isinstance(value, str): - len_value = len(value) - if len_value == 32 or len_value == 36: - return cls.from_uuid(UUID(value)) - if len_value == 26: - return cls.from_str(value) - if len_value == 16: - return cls.from_bytes(value) - if len_value == 10: - return cls.from_timestamp(int(value)) - raise ValueError( - f"Cannot create ULID from string of length {len_value}" - ) - if isinstance(value, (int, float)): - return cls.from_int(int(value)) - if isinstance(value, (bytes, bytearray)): - return cls.from_bytes(value) + return cls.parse_str(value) + if isinstance(value, int | float): + return cls.parse_int_float(value) + if isinstance(value, bytes | bytearray): + return cls.parse_bytes(value) if isinstance(value, memoryview): - return cls.from_bytes(value.tobytes()) - raise ValueError( - f"Cannot create ULID from type {value.__class__.__name__}" - ) + return cls.parse_memoryview(value) + msg = f"Cannot create ULID from type {value.__class__.__name__}" + raise ValueError(msg) + + @classmethod + def parse_ulid(cls, value: BaseULID) -> Self: + return value + + @classmethod + def parse_uuid(cls, value: UUID) -> Self: + return cls.from_uuid(value) + + @classmethod + def parse_str(cls, value: str) -> Self: + len_value = len(value) + if len_value in (UUID_LEN_WITHOUT_HYPHENS, UUID_LEN_WITH_HYPHENS): + return cls.from_uuid(UUID(value)) + if len_value == ULID_STR_LEN: + return cls.from_str(value) + if len_value == ULID_BYTES_LEN: + return cls.from_bytes(value.encode()) + if len_value == TIMESTAMP_STR_LEN: + return cls.from_timestamp(int(value)) + msg = f"Cannot create ULID from string of length {len_value}" + raise ValueError(msg) + + @classmethod + def parse_int_float(cls, value: int | float) -> Self: + return cls.from_int(int(value)) + + @classmethod + def parse_bytes(cls, value: bytes | bytearray) -> Self: + return cls.from_bytes(value) + + @classmethod + def parse_memoryview(cls, value: memoryview) -> Self: + return cls.from_bytes(value.tobytes()) diff --git a/care/utils/validation/integer_validation.py b/care/utils/validation/integer_validation.py deleted file mode 100644 index cfc75184e7..0000000000 --- a/care/utils/validation/integer_validation.py +++ /dev/null @@ -1,12 +0,0 @@ -from rest_framework.exceptions import ValidationError - - -def check_integer(vals): - if not isinstance(vals, list): - vals = [vals] - for i in range(len(vals)): - try: - vals[i] = int(vals[i]) - except Exception: - raise ValidationError({"value": "Integer Required"}) - return vals diff --git a/config/adminlogin.py b/config/adminlogin.py index 56e9bb0957..5d1d60c45b 100644 --- a/config/adminlogin.py +++ b/config/adminlogin.py @@ -12,7 +12,6 @@ def admin_login(request, **kwargs): request, "Too many login attempts, please try again in 20 minutes" ) return redirect(reverse("admin:index")) - else: - return login_func(request, **kwargs) + return login_func(request, **kwargs) return admin_login diff --git a/config/api_router.py b/config/api_router.py index bab899767c..917b187395 100644 --- a/config/api_router.py +++ b/config/api_router.py @@ -104,10 +104,7 @@ from care.users.api.viewsets.users import UserViewSet from care.users.api.viewsets.userskill import UserSkillViewSet -if settings.DEBUG: - router = DefaultRouter() -else: - router = SimpleRouter() +router = DefaultRouter() if settings.DEBUG else SimpleRouter() router.register("users", UserViewSet, basename="users") user_nested_router = NestedSimpleRouter(router, r"users", lookup="users") diff --git a/config/asgi.py b/config/asgi.py deleted file mode 100644 index 156e6f0e37..0000000000 --- a/config/asgi.py +++ /dev/null @@ -1,41 +0,0 @@ -""" -ASGI config for care project. - -It exposes the ASGI callable as a module-level variable named ``application``. - -For more information on this file, see -https://docs.djangoproject.com/en/dev/howto/deployment/asgi/ - -""" - -import os -import sys -from pathlib import Path - -from django.core.asgi import get_asgi_application - -# This allows easy placement of apps within the interior -# care directory. -BASE_DIR = Path(__file__).resolve(strict=True).parent.parent -sys.path.append(str(BASE_DIR / "care")) - -# If DJANGO_SETTINGS_MODULE is unset, default to the local settings -os.environ.setdefault("DJANGO_SETTINGS_MODULE", "config.settings.local") - -# This application object is used by any ASGI server configured to use this file. -django_application = get_asgi_application() -# Apply ASGI middleware here. -# from helloworld.asgi import HelloWorldApplication -# application = HelloWorldApplication(application) - -# Import websocket application here, so apps from django_application are loaded first -from config.websocket import websocket_application # noqa isort:skip - - -async def application(scope, receive, send): - if scope["type"] == "http": - await django_application(scope, receive, send) - elif scope["type"] == "websocket": - await websocket_application(scope, receive, send) - else: - raise NotImplementedError(f"Unknown scope type {scope['type']}") diff --git a/config/auth_views.py b/config/auth_views.py index 36412d618d..4391d37195 100644 --- a/config/auth_views.py +++ b/config/auth_views.py @@ -73,9 +73,8 @@ def validate(self, attrs): @classmethod def get_token(cls, user): - raise NotImplementedError( - "Must implement `get_token` method for `TokenObtainSerializer` subclasses" - ) + msg = "Must implement `get_token` method for `TokenObtainSerializer` subclasses" + raise NotImplementedError(msg) class TokenRefreshSerializer(serializers.Serializer): diff --git a/config/authentication.py b/config/authentication.py index 87d7501b2d..086348b1bc 100644 --- a/config/authentication.py +++ b/config/authentication.py @@ -1,6 +1,5 @@ import json import logging -from datetime import datetime import jwt import requests @@ -8,6 +7,7 @@ from django.contrib.auth.models import AnonymousUser from django.core.cache import cache from django.core.exceptions import ValidationError +from django.utils import timezone from django.utils.translation import gettext_lazy as _ from drf_spectacular.extensions import OpenApiAuthenticationExtension from drf_spectacular.plumbing import build_bearer_security_scheme_object @@ -24,6 +24,9 @@ logger = logging.getLogger(__name__) +OPENID_REQUEST_TIMEOUT = 5 + + def jwk_response_cache_key(url: str) -> str: return f"jwk_response:{url}" @@ -77,7 +80,7 @@ class MiddlewareAuthentication(JWTAuthentication): def get_public_key(self, url): public_key_json = cache.get(jwk_response_cache_key(url)) if not public_key_json: - res = requests.get(url) + res = requests.get(url, timeout=OPENID_REQUEST_TIMEOUT) res.raise_for_status() public_key_json = res.json() cache.set(jwk_response_cache_key(url), public_key_json, timeout=60 * 5) @@ -136,7 +139,7 @@ def get_raw_token(self, header): # Assume the header does not contain a JSON web token return None - if len(parts) != 2: + if len(parts) != 2: # noqa: PLR2004 raise AuthenticationFailed( _("Authorization header must contain two space-delimited values"), code="bad_authorization_header", @@ -190,7 +193,7 @@ def get_user(self, validated_token, facility): user_type=User.TYPE_VALUE_MAP["Nurse"], verified=True, asset=asset_obj, - date_of_birth=datetime.now().date(), + date_of_birth=timezone.now().date(), ) asset_user.save() return asset_user @@ -198,7 +201,7 @@ def get_user(self, validated_token, facility): class ABDMAuthentication(JWTAuthentication): def open_id_authenticate(self, url, token): - public_key = requests.get(url) + public_key = requests.get(url, timeout=OPENID_REQUEST_TIMEOUT) jwk = public_key.json()["keys"][0] public_key = jwt.algorithms.RSAAlgorithm.from_jwk(json.dumps(jwk)) return jwt.decode( @@ -227,7 +230,7 @@ def get_validated_token(self, url, token): return self.open_id_authenticate(url, token) except Exception as e: logger.info(e, "Token: ", token) - raise InvalidToken({"detail": f"Invalid Authorization token: {e}"}) + raise InvalidToken({"detail": "Invalid Authorization token"}) from e def get_user(self, validated_token): user = User.objects.filter(username=settings.ABDM_USERNAME).first() @@ -241,7 +244,7 @@ def get_user(self, validated_token): phone_number="917777777777", user_type=User.TYPE_VALUE_MAP["Volunteer"], verified=True, - date_of_birth=datetime.now().date(), + date_of_birth=timezone.now().date(), ) user.save() return user @@ -253,7 +256,9 @@ class CustomJWTAuthenticationScheme(OpenApiAuthenticationExtension): def get_security_definition(self, auto_schema): return build_bearer_security_scheme_object( - header_name="Authorization", token_prefix="Bearer", bearer_format="JWT" + header_name="Authorization", + token_prefix="Bearer", + bearer_format="JWT", ) diff --git a/config/health_views.py b/config/health_views.py index 99628d9c3c..7e0801bb74 100644 --- a/config/health_views.py +++ b/config/health_views.py @@ -9,14 +9,14 @@ class MiddlewareAuthenticationVerifyView(APIView): - authentication_classes = [MiddlewareAuthentication] + authentication_classes = (MiddlewareAuthentication,) def get(self, request): return Response(UserBaseMinimumSerializer(request.user).data) class MiddlewareAssetAuthenticationVerifyView(APIView): - authentication_classes = [MiddlewareAssetAuthentication] + authentication_classes = (MiddlewareAssetAuthentication,) def get(self, request): return Response(UserBaseMinimumSerializer(request.user).data) diff --git a/config/middleware.py b/config/middlewares.py similarity index 82% rename from config/middleware.py rename to config/middlewares.py index 3243201a56..aa8ffe091f 100644 --- a/config/middleware.py +++ b/config/middlewares.py @@ -11,5 +11,5 @@ def __call__(self, request): request.start_time = time.time() response = self.get_response(request) duration = time.time() - request.start_time - self.logger.info(f"Request to {request.path} took {duration:.4f} seconds") + self.logger.info("Request to %s took %.4f seconds", request.path, duration) return response diff --git a/config/ratelimit.py b/config/ratelimit.py index d1faadbf51..8fdbbb6fbd 100644 --- a/config/ratelimit.py +++ b/config/ratelimit.py @@ -2,8 +2,10 @@ from django.conf import settings from django_ratelimit.core import is_ratelimited +VALIDATE_CAPTCHA_REQUEST_TIMEOUT = 5 -def GETKEY(group, request): + +def get_ratelimit_key(group, request): return "ratelimit" @@ -16,44 +18,43 @@ def validatecaptcha(request): "response": recaptcha_response, } captcha_response = requests.post( - "https://www.google.com/recaptcha/api/siteverify", data=values + "https://www.google.com/recaptcha/api/siteverify", + data=values, + timeout=VALIDATE_CAPTCHA_REQUEST_TIMEOUT, ) result = captcha_response.json() - if result["success"] is True: - return True - return False + return bool(result["success"]) # refer https://django-ratelimit.readthedocs.io/en/stable/rates.html for rate def ratelimit( - request, group="", keys=[None], rate=settings.DJANGO_RATE_LIMIT, increment=True + request, group="", keys=None, rate=settings.DJANGO_RATE_LIMIT, increment=True ): + if keys is None: + keys = [None] if settings.DISABLE_RATELIMIT: return False checkcaptcha = False for key in keys: if key == "ip": - group = group - key = "ip" + _group = group + _key = "ip" else: - group = group + f"-{key}" - key = GETKEY + _group = group + f"-{key}" + _key = get_ratelimit_key if is_ratelimited( request, - group=group, - key=key, + group=_group, + key=_key, rate=rate, increment=increment, ): checkcaptcha = True if checkcaptcha: - if not validatecaptcha(request): - return True - else: - return False + return not validatecaptcha(request) return False diff --git a/config/serializers.py b/config/serializers.py deleted file mode 100644 index 5570ff8ad7..0000000000 --- a/config/serializers.py +++ /dev/null @@ -1,25 +0,0 @@ -from rest_framework import serializers - - -class ChoiceField(serializers.ChoiceField): - def to_representation(self, obj): - try: - return self._choices[obj] - except KeyError: - key_type = type(list(self.choices.keys())[0]) - key = key_type(obj) - return self._choices[key] - - def to_internal_value(self, data): - if isinstance(data, str) and data not in self.choice_strings_to_values: - choice_name_map = {v: k for k, v in self._choices.items()} - data = choice_name_map.get(data) - return super(ChoiceField, self).to_internal_value(data) - - -class MultipleChoiceField(serializers.MultipleChoiceField): - def to_representation(self, value): - return super(MultipleChoiceField, self).to_representation(value) - - def to_internal_value(self, data): - return super(MultipleChoiceField, self).to_internal_value(data) diff --git a/config/settings/base.py b/config/settings/base.py index 4c439790b6..5bf8ddd3b6 100644 --- a/config/settings/base.py +++ b/config/settings/base.py @@ -2,6 +2,7 @@ Base settings to build other settings files upon. """ +import logging from datetime import datetime, timedelta from pathlib import Path @@ -16,6 +17,8 @@ from care.utils.csp import config as csp_config from plug_config import manager +logger = logging.getLogger(__name__) + BASE_DIR = Path(__file__).resolve(strict=True).parent.parent.parent APPS_DIR = BASE_DIR / "care" env = environ.Env() @@ -197,7 +200,7 @@ # add RequestTimeLoggingMiddleware based on the environment variable if env.bool("ENABLE_REQUEST_TIME_LOGGING", default=False): - MIDDLEWARE.insert(0, "config.middleware.RequestTimeLoggingMiddleware") + MIDDLEWARE.insert(0, "config.middlewares.RequestTimeLoggingMiddleware") # STATIC # ------------------------------------------------------------------------------ @@ -275,7 +278,7 @@ CSRF_TRUSTED_ORIGINS = env.json("CSRF_TRUSTED_ORIGINS", default=[]) # https://github.com/adamchainz/django-cors-headers#cors_allowed_origin_regexes-sequencestr--patternstr -# CORS_URLS_REGEX = r"^/api/.*$" +# CORS_URLS_REGEX = r"^/api/.*$" # noqa: ERA001 # EMAIL # ------------------------------------------------------------------------------ @@ -303,9 +306,9 @@ # https://docs.djangoproject.com/en/dev/ref/settings/#server-email # SERVER_EMAIL = env("DJANGO_SERVER_EMAIL", default=DEFAULT_FROM_EMAIL) # noqa F405 # https://docs.djangoproject.com/en/dev/ref/settings/#admins -# ADMINS = [("""👪""", "admin@ohc.network")] +# ADMINS = [("""👪""", "admin@ohc.network")] # noqa: ERA001 # https://docs.djangoproject.com/en/dev/ref/settings/#managers -# MANAGERS = ADMINS +# MANAGERS = ADMINS # noqa: ERA001 # Django Admin URL. ADMIN_URL = env("DJANGO_ADMIN_URL", default="admin") @@ -379,7 +382,6 @@ "TITLE": "Care API", "DESCRIPTION": "Documentation of API endpoints of Care ", "VERSION": "1.0.0", - # "SERVE_PERMISSIONS": ["rest_framework.permissions.IsAdminUser"], } # Simple JWT (JWT Authentication) @@ -542,7 +544,7 @@ BUCKET_HAS_FINE_ACL = env.bool("BUCKET_HAS_FINE_ACL", default=False) if BUCKET_PROVIDER not in csp_config.CSProvider.__members__: - print(f"Warning Invalid CSP Found! {BUCKET_PROVIDER}") + logger.error("invalid CSP found: %s", BUCKET_PROVIDER) FILE_UPLOAD_BUCKET = env("FILE_UPLOAD_BUCKET", default="") FILE_UPLOAD_REGION = env("FILE_UPLOAD_REGION", default=BUCKET_REGION) @@ -615,10 +617,11 @@ # for setting the shifting mode PEACETIME_MODE = env.bool("PEACETIME_MODE", default=True) +# we are making this tz aware in the app so no need to make it aware here MIN_ENCOUNTER_DATE = env( "MIN_ENCOUNTER_DATE", - cast=lambda d: datetime.strptime(d, "%Y-%m-%d"), - default=datetime(2020, 1, 1), + cast=lambda d: datetime.strptime(d, "%Y-%m-%d"), # noqa: DTZ007 + default=datetime(2020, 1, 1), # noqa: DTZ001 ) # for exporting csv diff --git a/config/settings/local.py b/config/settings/local.py index 0edae64230..ccaf43208c 100644 --- a/config/settings/local.py +++ b/config/settings/local.py @@ -14,7 +14,7 @@ # WhiteNoise # ------------------------------------------------------------------------------ # http://whitenoise.evans.io/en/latest/django.html#using-whitenoise-in-development -INSTALLED_APPS = ["whitenoise.runserver_nostatic"] + INSTALLED_APPS +INSTALLED_APPS = ["whitenoise.runserver_nostatic", *INSTALLED_APPS] # django-silk # ------------------------------------------------------------------------------ diff --git a/config/urls.py b/config/urls.py index d06994121e..c90bd2adc2 100644 --- a/config/urls.py +++ b/config/urls.py @@ -72,9 +72,8 @@ path("health/", include("healthy_django.urls", namespace="healthy_django")), # OpenID Connect path(".well-known/jwks.json", PublicJWKsView.as_view(), name="jwks-json"), - # TODO: Remove the config url as its not a standard implementation - path(".well-known/openid-configuration", PublicJWKsView.as_view()), -] + static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT) + *static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT), +] if settings.ENABLE_ABDM: urlpatterns += abdm_urlpatterns diff --git a/config/utils.py b/config/utils.py deleted file mode 100644 index be3feadf2d..0000000000 --- a/config/utils.py +++ /dev/null @@ -1,2 +0,0 @@ -def get_psql_search_tokens(text, operator="&"): - return f" {operator} ".join([f"{word}:*" for word in text.strip().split(" ")]) diff --git a/config/validators.py b/config/validators.py deleted file mode 100644 index 754cb99496..0000000000 --- a/config/validators.py +++ /dev/null @@ -1,69 +0,0 @@ -import re - -from django.core.exceptions import ValidationError -from django.core.validators import RegexValidator -from django.utils.translation import gettext_lazy as _ - - -class NumberValidator: - def validate(self, password, user=None): - if not re.findall(r"\d", password): - raise ValidationError( - _("The password must contain at least 1 digit, 0-9."), - code="password_no_number", - ) - - def get_help_text(self): - return _("Your password must contain at least 1 digit, 0-9.") - - -class UppercaseValidator: - def validate(self, password, user=None): - if not re.findall("[A-Z]", password): - raise ValidationError( - _("The password must contain at least 1 uppercase letter, A-Z."), - code="password_no_upper", - ) - - def get_help_text(self): - return _("Your password must contain at least 1 uppercase letter, A-Z.") - - -class LowercaseValidator: - def validate(self, password, user=None): - if not re.findall("[a-z]", password): - raise ValidationError( - _("The password must contain at least 1 lowercase letter, a-z."), - code="password_no_lower", - ) - - def get_help_text(self): - return _("Your password must contain at least 1 lowercase letter, a-z.") - - -class SymbolValidator: - def validate(self, password, user=None): - if not re.findall(r"[()[\]{}|\\`~!@#$%^&*_\-+=;:'\",<>./?]", password): - raise ValidationError( - _( - "The password must contain at least 1 symbol: " - + r"()[]{}|\`~!@#$%^&*_-+=;:'\",<>./?" - ), - code="password_no_symbol", - ) - - def get_help_text(self): - return _( - "Your password must contain at least 1 symbol: " - + r"()[]{}|\`~!@#$%^&*_-+=;:'\",<>./?" - ) - - -class MiddlewareDomainAddressValidator(RegexValidator): - regex = r"^(?!https?:\/\/)[a-zA-Z0-9_-]+(\.[a-zA-Z0-9_-]+)*\.[a-zA-Z]{2,}$" - code = "invalid_domain_name" - message = _( - "The domain name is invalid. " - "It should not start with scheme and " - "should not end with a trailing slash." - ) diff --git a/config/views.py b/config/views.py index 1cd9d3e310..30bde91d67 100644 --- a/config/views.py +++ b/config/views.py @@ -3,7 +3,7 @@ from django.shortcuts import render -def app_version(_): +def app_version(request): return JsonResponse({"version": settings.APP_VERSION}) @@ -11,5 +11,5 @@ def home_view(request): return render(request, "pages/home.html") -def ping(_): +def ping(request): return JsonResponse({"status": "OK"}) diff --git a/config/websocket.py b/config/websocket.py deleted file mode 100644 index 81adfbc664..0000000000 --- a/config/websocket.py +++ /dev/null @@ -1,13 +0,0 @@ -async def websocket_application(scope, receive, send): - while True: - event = await receive() - - if event["type"] == "websocket.connect": - await send({"type": "websocket.accept"}) - - if event["type"] == "websocket.disconnect": - break - - if event["type"] == "websocket.receive": - if event["text"] == "ping": - await send({"type": "websocket.send", "text": "pong!"}) diff --git a/config/wsgi.py b/config/wsgi.py index c9f43409b4..31a9197142 100644 --- a/config/wsgi.py +++ b/config/wsgi.py @@ -27,7 +27,7 @@ # We defer to a DJANGO_SETTINGS_MODULE already in the environment. This breaks # if running multiple sites in the same mod_wsgi process. To fix this, use # mod_wsgi daemon mode with each site in its own daemon process, or use -# os.environ["DJANGO_SETTINGS_MODULE"] = "config.settings.production" +# os.environ["DJANGO_SETTINGS_MODULE"] = "config.settings.production" # noqa: ERA001 os.environ.setdefault("DJANGO_SETTINGS_MODULE", "config.settings.production") # This application object is used by any WSGI server configured to use this @@ -35,5 +35,5 @@ # setting points here. application = get_wsgi_application() # Apply WSGI middleware here. -# from helloworld.wsgi import HelloWorldApplication -# application = HelloWorldApplication(application) +# from helloworld.wsgi import HelloWorldApplication # noqa: ERA001 +# application = HelloWorldApplication(application) # noqa: ERA001 diff --git a/docs/conf.py b/docs/conf.py index 5bbd458924..0c47d8089c 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -10,19 +10,11 @@ # add these directories to sys.path here. If the directory is relative to the # documentation root, use os.path.abspath to make it absolute, like shown here. # -# import os -# import sys - -# import django -# sys.path.insert(0, os.path.abspath('..')) -# os.environ.setdefault("DJANGO_SETTINGS_MODULE", "config.settings.local") -# django.setup() - # -- Project information ----------------------------------------------------- project = "Care" -copyright = """2023, Open Healthcare Network""" +copyright = """2023, Open Healthcare Network""" # noqa: A001 author = "ohcnetwork" diff --git a/manage.py b/manage.py index 73f24a795d..fdfa506885 100755 --- a/manage.py +++ b/manage.py @@ -1,5 +1,6 @@ #!/usr/bin/env python """Django's command-line utility for administrative tasks.""" + import os import sys @@ -19,11 +20,12 @@ def main(): try: from django.core.management import execute_from_command_line except ImportError as exc: - raise ImportError( + msg = ( "Couldn't import Django. Are you sure it's installed and " "available on your PYTHONPATH environment variable? Did you " "forget to activate a virtual environment?" - ) from exc + ) + raise ImportError(msg) from exc execute_from_command_line(sys.argv) diff --git a/merge_production_dotenvs_in_dotenv.py b/merge_production_dotenvs_in_dotenv.py deleted file mode 100644 index 6fdb55325c..0000000000 --- a/merge_production_dotenvs_in_dotenv.py +++ /dev/null @@ -1,30 +0,0 @@ -import os -from collections.abc import Sequence - -ROOT_DIR_PATH = os.path.dirname(os.path.realpath(__file__)) -PRODUCTION_DOTENVS_DIR_PATH = os.path.join(ROOT_DIR_PATH, ".envs", ".production") -PRODUCTION_DOTENV_FILE_PATHS = [ - os.path.join(PRODUCTION_DOTENVS_DIR_PATH, ".django"), - os.path.join(PRODUCTION_DOTENVS_DIR_PATH, ".postgres"), -] -DOTENV_FILE_PATH = os.path.join(ROOT_DIR_PATH, ".env") - - -def merge( - output_file_path: str, merged_file_paths: Sequence[str], append_linesep: bool = True -) -> None: - with open(output_file_path, "w") as output_file: - for merged_file_path in merged_file_paths: - with open(merged_file_path) as merged_file: - merged_file_content = merged_file.read() - output_file.write(merged_file_content) - if append_linesep: - output_file.write(os.linesep) - - -def main(): - merge(DOTENV_FILE_PATH, PRODUCTION_DOTENV_FILE_PATHS) - - -if __name__ == "__main__": - main() diff --git a/pyproject.toml b/pyproject.toml index 2ab41f7ca4..93d676ccd2 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -21,7 +21,7 @@ ignore_errors = true [tool.ruff] target-version = "py312" -extend-exclude = ["*/migrations_old/*"] +extend-exclude = ["*/migrations*/*", "care/abdm/*"] include = ["*.py", "pyproject.toml"] [tool.ruff.lint] @@ -34,34 +34,34 @@ select = [ "N", # pep8-naming "UP", # pyupgrade # "ANN", # flake8-annotations - "S", # flake8-bandit - "FBT", # flake8-boolean-trap - "B", # flake8-bugbear - "A", # flake8-builtins - "COM", # flake8-commas - "C4", # flake8-comprehensions - "DTZ", # flake8-datetimez - "T10", # flake8-debugger - "DJ", # flake8-django - "EM", # flake8-errmsg - "ISC", # flake8-import-conventions - "ICN", # flake8-import-order - "LOG", # flake8-logging - "G", # flake8-logging-format - "INP", # flake8-no-pep420 - "PIE", # flake8-pie - "T20", # flake8-print - "Q", # flake8-quotes - "RSE", # flake8-raise - "RET", # flake8-return - "SLF", # flake8-self - "SIM", # flake8-simplify - "TID", # flake8-tidy-imports - "TCH", # flake8-todo - "INT", # flake8-gettext - "ARG", # flake8-unused-arguments + "S", # flake8-bandit + "FBT", # flake8-boolean-trap + "B", # flake8-bugbear + "A", # flake8-builtins + "COM", # flake8-commas + "C4", # flake8-comprehensions + "DTZ", # flake8-datetimez + "T10", # flake8-debugger + "DJ", # flake8-django + "EM", # flake8-errmsg + "ISC", # flake8-import-conventions + "ICN", # flake8-import-order + "LOG", # flake8-logging + "G", # flake8-logging-format + "INP", # flake8-no-pep420 + "PIE", # flake8-pie + "T20", # flake8-print + "Q", # flake8-quotes + "RSE", # flake8-raise + "RET", # flake8-return + "SLF", # flake8-self + "SIM", # flake8-simplify + "TID", # flake8-tidy-imports + "TCH", # flake8-todo + "INT", # flake8-gettext + # "ARG", # flake8-unused-arguments "PTH", # flake8-use-pathlib - "TD", # flake8-todo + # "TD", # flake8-todo # disabling this for now "ERA", # eradicate "PL", # pylint "FURB", # refurb @@ -79,6 +79,10 @@ ignore = [ "DJ001", # django-nullable-model-string-field "ISC001", # conflicts with format "COM812", # conflicts with format + "RUF012", # Too hard + "FBT001", # why not! + "S106", + "S105", ] @@ -86,6 +90,10 @@ ignore = [ line-ending = "lf" +[tool.ruff.lint.per-file-ignores] +"**/__init__.py" = ["E402", "F401"] # for imports +"**/tests/**" = ["DTZ001"] + [tool.ruff.lint.flake8-builtins] builtins-ignorelist = ["id", "list", "filter"] @@ -94,6 +102,5 @@ builtins-ignorelist = ["id", "list", "filter"] docstring-quotes = "double" -[tool.ruff.lint.per-file-ignores] -"**/__init__.py" = ["E402", "F401"] # for imports -"**/tests/**" = ["DTZ001"] +[tool.ruff.lint.flake8-unused-arguments] +ignore-variadic-names = true