diff --git a/.github/workflows/update-jira-tickets.yaml b/.github/workflows/update-jira-tickets.yaml index bf30629e1ea..b9c11b48f67 100644 --- a/.github/workflows/update-jira-tickets.yaml +++ b/.github/workflows/update-jira-tickets.yaml @@ -103,6 +103,8 @@ jobs: echo "tickets=${jiraTickets}" >> $GITHUB_OUTPUT - name: Periodically ping Jenkins for current tag build status + env: + REPO_URL: "${{ github.server_url }}/${{ github.repository }}" run: | repoName=${GITHUB_REPOSITORY##*/} currentTag="${{ steps.get-tag.outputs.tag }}" @@ -124,7 +126,7 @@ jobs: if [[ "$result" == "SUCCESS" ]]; then echo "Build successful! Submitting ticket numbers to Jira" tickets="${{ steps.jira-tickets.outputs.tickets }}" - json="{ \"issues\": $(echo "${tickets}" | jq -R -s -c 'split(" ")[:-1]') }" + json="{ \"issues\": $(echo "${tickets}" | jq -R -s -c 'split(" ")[:-1]'), \"data\": { \"tag\": \"${currentTag}\", \"repository\": \"${REPO_URL}\" } }" curl -X POST -H 'Content-Type: application/json' --url "${JIRA_WEBHOOK_URL}" --data "$json" break elif [[ "$result" != "null" ]]; then diff --git a/Pipfile b/Pipfile index c1b96a21a5d..161b11d5e8a 100644 --- a/Pipfile +++ b/Pipfile @@ -4,20 +4,21 @@ verify_ssl = true name = "pypi" [packages] -aio-pika = "==9.4.3" +aio-pika = "==9.5.3" aiofiles = "==24.1.0" -aiohttp = "==3.10.10" -alembic = "==1.13.3" +aiohttp = "==3.11.9" +alembic = "==1.14.0" asyncpg = "==0.30.0" -azure-storage-blob = "==12.23.1" -bcrypt = "==4.2.0" -boto3 = "==1.35.47" -fastapi = "==0.115.3" +azure-storage-blob = "==12.24.0" +bcrypt = "==4.2.1" +boto3 = "==1.35.77" +fastapi = "==0.115.6" fastapi-mail = "==1.2.9" firebase-admin = "==6.5.0" -httpx = "==0.27.2" +httpx = "==0.28.0" jinja2 = "==3.1.4" -nh3 = "==0.2.18" +more-itertools = "==10.5.0" +nh3 = "==0.2.19" opentelemetry-api = "==1.27.0" opentelemetry-distro = "==0.48b0" opentelemetry-exporter-otlp = "==1.27.0" @@ -37,11 +38,12 @@ opentelemetry-sdk-extension-aws = "==2.0.2" opentelemetry-semantic-conventions = "==0.48b0" opentelemetry-test-utils = "==0.48b0" opentelemetry-util-http = "==0.48b0" -pyOpenSSL = "==24.2.1" pydantic = { extras = ["email"], version = "==1.10.18" } -pymongo = "*" -python-multipart = "==0.0.12" -redis = "==5.1.1" +pyjwt = "==2.10.1" +pymongo = "==4.10.1" +pyOpenSSL = "==24.2.1" +python-multipart = "==0.0.19" +redis = "==5.2.0" sentry-sdk = "~=2.13" sqlalchemy = { extras = ["asyncio"], version = "==1.4.53" } sqlalchemy-utils = "==0.41.2" @@ -57,32 +59,33 @@ bytecode = "==0.16.0" structlog = "==24.4.0" asgi-correlation-id = "==4.3.4" + [dev-packages] +allure-pytest = "==2.13.5" +cachetools = "==5.3.0" +gevent = "==24.2.1" +greenlet = "==3.1.0" ipdb = "==0.13.13" -pudb = "==2024.1.3" +mypy = "==1.13.0" +nest-asyncio = "==1.6.0" pre-commit = "==4.0.1" -ruff = "==0.7.0" -allure-pytest = "==2.13.5" +pudb = "==2024.1.3" pydantic-factories = "==1.17.3" -pytest = "==8.3.3" +pyld = "==2.0.4" +pytest = "==8.3.4" pytest-asyncio = "~=0.19" -pytest-cov = "==5.0.0" +pytest-cov = "==6.0.0" pytest-env = "==1.1.5" pytest-lazy-fixtures = "==1.1.1" pytest-mock = "==3.14.0" -nest-asyncio = "==1.6.0" -gevent = "==24.2.1" -mypy = "==1.13.0" -types-python-dateutil = "==2.9.0.20241003" -typing-extensions = "==4.12.2" -types-requests = "==2.32.0.20241016" -types-pytz = "==2024.2.0.20241003" +reproschema = "==0.6.2" +ruff = "==0.8.2" types-aiofiles = "==24.1.0.20240626" types-cachetools = "==5.5.0.20240820" -greenlet = "==3.1.0" -reproschema = "*" -cachetools = "==5.3.0" -pyld = "==2.0.4" +types-python-dateutil = "==2.9.0.20241206 " +types-pytz = "==2024.2.0.20241003" +types-requests = "==2.32.0.20241016" +typing-extensions = "==4.12.2" [requires] python_version = "3.11" diff --git a/Pipfile.lock b/Pipfile.lock index 98da6b73c81..918ec77a790 100644 --- a/Pipfile.lock +++ b/Pipfile.lock @@ -18,12 +18,12 @@ "default": { "aio-pika": { "hashes": [ - "sha256:f1423d2d5a8b7315d144efe1773763bf687ac17aa1535385982687e9e5ed49bb", - "sha256:fd2b1fce25f6ed5203ef1dd554dc03b90c9a46a64aaf758d032d78dc31e5295d" + "sha256:95c19605ad2918e46ae39ead9a4991d5a8738dceedef11ff352b62eb3f935f36", + "sha256:9cdbc3350a76a04947348f07fa606bb76bb2d1bcb36523cc5452210e32a5041b" ], "index": "pypi", - "markers": "python_version >= '3.8' and python_version < '4.0'", - "version": "==9.4.3" + "markers": "python_version >= '3.9' and python_version < '4.0'", + "version": "==9.5.3" }, "aiofiles": { "hashes": [ @@ -44,101 +44,86 @@ }, "aiohttp": { "hashes": [ - "sha256:007ec22fbc573e5eb2fb7dec4198ef8f6bf2fe4ce20020798b2eb5d0abda6138", - "sha256:00819de9e45d42584bed046314c40ea7e9aea95411b38971082cad449392b08c", - "sha256:01948b1d570f83ee7bbf5a60ea2375a89dfb09fd419170e7f5af029510033d24", - "sha256:038f514fe39e235e9fef6717fbf944057bfa24f9b3db9ee551a7ecf584b5b480", - "sha256:03a42ac7895406220124c88911ebee31ba8b2d24c98507f4a8bf826b2937c7f2", - "sha256:05646ebe6b94cc93407b3bf34b9eb26c20722384d068eb7339de802154d61bc5", - "sha256:0631dd7c9f0822cc61c88586ca76d5b5ada26538097d0f1df510b082bad3411a", - "sha256:0b00807e2605f16e1e198f33a53ce3c4523114059b0c09c337209ae55e3823a8", - "sha256:0e1b370d8007c4ae31ee6db7f9a2fe801a42b146cec80a86766e7ad5c4a259cf", - "sha256:15ecd889a709b0080f02721255b3f80bb261c2293d3c748151274dfea93ac871", - "sha256:1b66ccafef7336a1e1f0e389901f60c1d920102315a56df85e49552308fc0486", - "sha256:1bbb122c557a16fafc10354b9d99ebf2f2808a660d78202f10ba9d50786384b9", - "sha256:1eb89d3d29adaf533588f209768a9c02e44e4baf832b08118749c5fad191781d", - "sha256:258c5dd01afc10015866114e210fb7365f0d02d9d059c3c3415382ab633fcbcb", - "sha256:2609e9ab08474702cc67b7702dbb8a80e392c54613ebe80db7e8dbdb79837c68", - "sha256:274cfa632350225ce3fdeb318c23b4a10ec25c0e2c880eff951a3842cf358ac1", - "sha256:28529e08fde6f12eba8677f5a8608500ed33c086f974de68cc65ab218713a59d", - "sha256:2b606353da03edcc71130b52388d25f9a30a126e04caef1fd637e31683033abd", - "sha256:30ca7c3b94708a9d7ae76ff281b2f47d8eaf2579cd05971b5dc681db8caac6e1", - "sha256:333cf6cf8e65f6a1e06e9eb3e643a0c515bb850d470902274239fea02033e9a8", - "sha256:3455522392fb15ff549d92fbf4b73b559d5e43dc522588f7eb3e54c3f38beee7", - "sha256:362f641f9071e5f3ee6f8e7d37d5ed0d95aae656adf4ef578313ee585b585959", - "sha256:3bcd391d083f636c06a68715e69467963d1f9600f85ef556ea82e9ef25f043f7", - "sha256:3dffb610a30d643983aeb185ce134f97f290f8935f0abccdd32c77bed9388b42", - "sha256:3fe407bf93533a6fa82dece0e74dbcaaf5d684e5a51862887f9eaebe6372cd79", - "sha256:413251f6fcf552a33c981c4709a6bba37b12710982fec8e558ae944bfb2abd38", - "sha256:438cd072f75bb6612f2aca29f8bd7cdf6e35e8f160bc312e49fbecab77c99e3a", - "sha256:4470c73c12cd9109db8277287d11f9dd98f77fc54155fc71a7738a83ffcc8ea8", - "sha256:45c3b868724137f713a38376fef8120c166d1eadd50da1855c112fe97954aed8", - "sha256:486f7aabfa292719a2753c016cc3a8f8172965cabb3ea2e7f7436c7f5a22a151", - "sha256:4f05e9727ce409358baa615dbeb9b969db94324a79b5a5cea45d39bdb01d82e6", - "sha256:50aed5155f819873d23520919e16703fc8925e509abbb1a1491b0087d1cd969e", - "sha256:50edbcad60d8f0e3eccc68da67f37268b5144ecc34d59f27a02f9611c1d4eec7", - "sha256:54ca74df1be3c7ca1cf7f4c971c79c2daf48d9aa65dea1a662ae18926f5bc8ce", - "sha256:578a4b875af3e0daaf1ac6fa983d93e0bbfec3ead753b6d6f33d467100cdc67b", - "sha256:597a079284b7ee65ee102bc3a6ea226a37d2b96d0418cc9047490f231dc09fe8", - "sha256:59bb3c54aa420521dc4ce3cc2c3fe2ad82adf7b09403fa1f48ae45c0cbde6628", - "sha256:5c6a5b8c7926ba5d8545c7dd22961a107526562da31a7a32fa2456baf040939f", - "sha256:64f6c17757251e2b8d885d728b6433d9d970573586a78b78ba8929b0f41d045a", - "sha256:679abe5d3858b33c2cf74faec299fda60ea9de62916e8b67e625d65bf069a3b7", - "sha256:741a46d58677d8c733175d7e5aa618d277cd9d880301a380fd296975a9cdd7bc", - "sha256:7789050d9e5d0c309c706953e5e8876e38662d57d45f936902e176d19f1c58ab", - "sha256:77abf6665ae54000b98b3c742bc6ea1d1fb31c394bcabf8b5d2c1ac3ebfe7f3b", - "sha256:79019094f87c9fb44f8d769e41dbb664d6e8fcfd62f665ccce36762deaa0e911", - "sha256:7b06b7843929e41a94ea09eb1ce3927865387e3e23ebe108e0d0d09b08d25be9", - "sha256:7e338c0523d024fad378b376a79faff37fafb3c001872a618cde1d322400a572", - "sha256:7ea7ffc6d6d6f8a11e6f40091a1040995cdff02cfc9ba4c2f30a516cb2633554", - "sha256:8105fd8a890df77b76dd3054cddf01a879fc13e8af576805d667e0fa0224c35d", - "sha256:84afcdea18eda514c25bc68b9af2a2b1adea7c08899175a51fe7c4fb6d551257", - "sha256:9294bbb581f92770e6ed5c19559e1e99255e4ca604a22c5c6397b2f9dd3ee42c", - "sha256:93429602396f3383a797a2a70e5f1de5df8e35535d7806c9f91df06f297e109b", - "sha256:9627cc1a10c8c409b5822a92d57a77f383b554463d1884008e051c32ab1b3742", - "sha256:998f3bd3cfc95e9424a6acd7840cbdd39e45bc09ef87533c006f94ac47296090", - "sha256:9c72109213eb9d3874f7ac8c0c5fa90e072d678e117d9061c06e30c85b4cf0e6", - "sha256:9fc1500fd2a952c5c8e3b29aaf7e3cc6e27e9cfc0a8819b3bce48cc1b849e4cc", - "sha256:a3f00003de6eba42d6e94fabb4125600d6e484846dbf90ea8e48a800430cc142", - "sha256:a45d85cf20b5e0d0aa5a8dca27cce8eddef3292bc29d72dcad1641f4ed50aa16", - "sha256:a7d8d14fe962153fc681f6366bdec33d4356f98a3e3567782aac1b6e0e40109a", - "sha256:a8fa23fe62c436ccf23ff930149c047f060c7126eae3ccea005f0483f27b2e28", - "sha256:aa6658732517ddabe22c9036479eabce6036655ba87a0224c612e1ae6af2087e", - "sha256:aafc8ee9b742ce75044ae9a4d3e60e3d918d15a4c2e08a6c3c3e38fa59b92d94", - "sha256:ab5a5a0c7a7991d90446a198689c0535be89bbd6b410a1f9a66688f0880ec026", - "sha256:acd48d5b80ee80f9432a165c0ac8cbf9253eaddb6113269a5e18699b33958dbb", - "sha256:ad7593bb24b2ab09e65e8a1d385606f0f47c65b5a2ae6c551db67d6653e78c28", - "sha256:baa42524a82f75303f714108fea528ccacf0386af429b69fff141ffef1c534f9", - "sha256:bdfcf6443637c148c4e1a20c48c566aa694fa5e288d34b20fcdc58507882fed3", - "sha256:be7443669ae9c016b71f402e43208e13ddf00912f47f623ee5994e12fc7d4b3f", - "sha256:c02a30b904282777d872266b87b20ed8cc0d1501855e27f831320f471d54d983", - "sha256:c1277cd707c465cd09572a774559a3cc7c7a28802eb3a2a9472588f062097205", - "sha256:c30a0eafc89d28e7f959281b58198a9fa5e99405f716c0289b7892ca345fe45f", - "sha256:c5ce2ce7c997e1971b7184ee37deb6ea9922ef5163c6ee5aa3c274b05f9e12fa", - "sha256:c823bc3971c44ab93e611ab1a46b1eafeae474c0c844aff4b7474287b75fe49c", - "sha256:ce0cdc074d540265bfeb31336e678b4e37316849d13b308607efa527e981f5c2", - "sha256:d1720b4f14c78a3089562b8875b53e36b51c97c51adc53325a69b79b4b48ebcb", - "sha256:d183cf9c797a5291e8301790ed6d053480ed94070637bfaad914dd38b0981f67", - "sha256:d9010c31cd6fa59438da4e58a7f19e4753f7f264300cd152e7f90d4602449762", - "sha256:d9e5e4a85bdb56d224f412d9c98ae4cbd032cc4f3161818f692cd81766eee65a", - "sha256:da1dee8948d2137bb51fbb8a53cce6b1bcc86003c6b42565f008438b806cccd8", - "sha256:df9270660711670e68803107d55c2b5949c2e0f2e4896da176e1ecfc068b974a", - "sha256:e00e3505cd80440f6c98c6d69269dcc2a119f86ad0a9fd70bccc59504bebd68a", - "sha256:e48d5021a84d341bcaf95c8460b152cfbad770d28e5fe14a768988c461b821bc", - "sha256:e7f8b04d83483577fd9200461b057c9f14ced334dcb053090cea1da9c8321a91", - "sha256:edfe3341033a6b53a5c522c802deb2079eee5cbfbb0af032a55064bd65c73a23", - "sha256:ef9c33cc5cbca35808f6c74be11eb7f5f6b14d2311be84a15b594bd3e58b5527", - "sha256:f2d4324a98062be0525d16f768a03e0bbb3b9fe301ceee99611dc9a7953124e6", - "sha256:f3935f82f6f4a3820270842e90456ebad3af15810cf65932bd24da4463bc0a4c", - "sha256:f614ab0c76397661b90b6851a030004dac502e48260ea10f2441abd2207fbcc7", - "sha256:f7db54c7914cc99d901d93a34704833568d86c20925b2762f9fa779f9cd2e70f", - "sha256:fbc6264158392bad9df19537e872d476f7c57adf718944cc1e4495cbabf38e2a", - "sha256:fe2fb38c2ed905a2582948e2de560675e9dfbee94c6d5ccdb1301c6d0a5bf092", - "sha256:ffe595f10566f8276b76dc3a11ae4bb7eba1aac8ddd75811736a15b0d5311414" - ], - "index": "pypi", - "markers": "python_version >= '3.8'", - "version": "==3.10.10" + "sha256:0411777249f25d11bd2964a230b3ffafcbed6cd65d0f2b132bc2b8f5b8c347c7", + "sha256:0a97d657f6cf8782a830bb476c13f7d777cfcab8428ac49dde15c22babceb361", + "sha256:0b5a5009b0159a8f707879dc102b139466d8ec6db05103ec1520394fdd8ea02c", + "sha256:0bcb7f6976dc0b6b56efde13294862adf68dd48854111b422a336fa729a82ea6", + "sha256:14624d96f0d69cf451deed3173079a68c322279be6030208b045ab77e1e8d550", + "sha256:15c4e489942d987d5dac0ba39e5772dcbed4cc9ae3710d1025d5ba95e4a5349c", + "sha256:176f8bb8931da0613bb0ed16326d01330066bb1e172dd97e1e02b1c27383277b", + "sha256:17af09d963fa1acd7e4c280e9354aeafd9e3d47eaa4a6bfbd2171ad7da49f0c5", + "sha256:1a8b13b9950d8b2f8f58b6e5842c4b842b5887e2c32e3f4644d6642f1659a530", + "sha256:202f40fb686e5f93908eee0c75d1e6fbe50a43e9bd4909bf3bf4a56b560ca180", + "sha256:21cbe97839b009826a61b143d3ca4964c8590d7aed33d6118125e5b71691ca46", + "sha256:27935716f8d62c1c73010428db310fd10136002cfc6d52b0ba7bdfa752d26066", + "sha256:282e0a7ddd36ebc411f156aeaa0491e8fe7f030e2a95da532cf0c84b0b70bc66", + "sha256:28f29bce89c3b401a53d6fd4bee401ee943083bf2bdc12ef297c1d63155070b0", + "sha256:2ac9fd83096df36728da8e2f4488ac3b5602238f602706606f3702f07a13a409", + "sha256:30f9f89ae625d412043f12ca3771b2ccec227cc93b93bb1f994db6e1af40a7d3", + "sha256:317251b9c9a2f1a9ff9cd093775b34c6861d1d7df9439ce3d32a88c275c995cd", + "sha256:31de2f10f63f96cc19e04bd2df9549559beadd0b2ee2da24a17e7ed877ca8c60", + "sha256:36df00e0541f264ce42d62280281541a47474dfda500bc5b7f24f70a7f87be7a", + "sha256:39625703540feb50b6b7f938b3856d1f4886d2e585d88274e62b1bd273fae09b", + "sha256:3f5461c77649358610fb9694e790956b4238ac5d9e697a17f63619c096469afe", + "sha256:4313f3bc901255b22f01663eeeae167468264fdae0d32c25fc631d5d6e15b502", + "sha256:442356e8924fe1a121f8c87866b0ecdc785757fd28924b17c20493961b3d6697", + "sha256:44cb1a1326a0264480a789e6100dc3e07122eb8cd1ad6b784a3d47d13ed1d89c", + "sha256:44d323aa80a867cb6db6bebb4bbec677c6478e38128847f2c6b0f70eae984d72", + "sha256:499368eb904566fbdf1a3836a1532000ef1308f34a1bcbf36e6351904cced771", + "sha256:4b01d9cfcb616eeb6d40f02e66bebfe7b06d9f2ef81641fdd50b8dd981166e0b", + "sha256:5720ebbc7a1b46c33a42d489d25d36c64c419f52159485e55589fbec648ea49a", + "sha256:5cc5e0d069c56645446c45a4b5010d4b33ac6c5ebfd369a791b5f097e46a3c08", + "sha256:618b18c3a2360ac940a5503da14fa4f880c5b9bc315ec20a830357bcc62e6bae", + "sha256:6435a66957cdba1a0b16f368bde03ce9c79c57306b39510da6ae5312a1a5b2c1", + "sha256:647ec5bee7e4ec9f1034ab48173b5fa970d9a991e565549b965e93331f1328fe", + "sha256:6e1e9e447856e9b7b3d38e1316ae9a8c92e7536ef48373de758ea055edfd5db5", + "sha256:6ef1550bb5f55f71b97a6a395286db07f7f2c01c8890e613556df9a51da91e8d", + "sha256:6ffa45cc55b18d4ac1396d1ddb029f139b1d3480f1594130e62bceadf2e1a838", + "sha256:77f31cebd8c27a36af6c7346055ac564946e562080ee1a838da724585c67474f", + "sha256:7a3b5b2c012d70c63d9d13c57ed1603709a4d9d7d473e4a9dfece0e4ea3d5f51", + "sha256:7a7ddf981a0b953ade1c2379052d47ccda2f58ab678fca0671c7c7ca2f67aac2", + "sha256:84de955314aa5e8d469b00b14d6d714b008087a0222b0f743e7ffac34ef56aff", + "sha256:8dcfd14c712aa9dd18049280bfb2f95700ff6a8bde645e09f17c3ed3f05a0130", + "sha256:928f92f80e2e8d6567b87d3316c1fd9860ccfe36e87a9a7f5237d4cda8baa1ba", + "sha256:9384b07cfd3045b37b05ed002d1c255db02fb96506ad65f0f9b776b762a7572e", + "sha256:96726839a42429318017e67a42cca75d4f0d5248a809b3cc2e125445edd7d50d", + "sha256:96bbec47beb131bbf4bae05d8ef99ad9e5738f12717cfbbf16648b78b0232e87", + "sha256:9bcf97b971289be69638d8b1b616f7e557e1342debc7fc86cf89d3f08960e411", + "sha256:a0cf4d814689e58f57ecd5d8c523e6538417ca2e72ff52c007c64065cef50fb2", + "sha256:a7c6147c6306f537cff59409609508a1d2eff81199f0302dd456bb9e7ea50c39", + "sha256:a9266644064779840feec0e34f10a89b3ff1d2d6b751fe90017abcad1864fa7c", + "sha256:afbe85b50ade42ddff5669947afde9e8a610e64d2c80be046d67ec4368e555fa", + "sha256:afcda759a69c6a8be3aae764ec6733155aa4a5ad9aad4f398b52ba4037942fe3", + "sha256:b2fab23003c4bb2249729a7290a76c1dda38c438300fdf97d4e42bf78b19c810", + "sha256:bd3f711f4c99da0091ced41dccdc1bcf8be0281dc314d6d9c6b6cf5df66f37a9", + "sha256:be0c7c98e38a1e3ad7a6ff64af8b6d6db34bf5a41b1478e24c3c74d9e7f8ed42", + "sha256:c1f2d7fd583fc79c240094b3e7237d88493814d4b300d013a42726c35a734bc9", + "sha256:c5bba6b83fde4ca233cfda04cbd4685ab88696b0c8eaf76f7148969eab5e248a", + "sha256:c6beeac698671baa558e82fa160be9761cf0eb25861943f4689ecf9000f8ebd0", + "sha256:c7333e7239415076d1418dbfb7fa4df48f3a5b00f8fdf854fca549080455bc14", + "sha256:c8a02f74ae419e3955af60f570d83187423e42e672a6433c5e292f1d23619269", + "sha256:c9c23e62f3545c2216100603614f9e019e41b9403c47dd85b8e7e5015bf1bde0", + "sha256:cca505829cdab58c2495ff418c96092d225a1bbd486f79017f6de915580d3c44", + "sha256:d3108f0ad5c6b6d78eec5273219a5bbd884b4aacec17883ceefaac988850ce6e", + "sha256:d4b8a1b6c7a68c73191f2ebd3bf66f7ce02f9c374e309bdb68ba886bbbf1b938", + "sha256:d6e274661c74195708fc4380a4ef64298926c5a50bb10fbae3d01627d7a075b7", + "sha256:db2914de2559809fdbcf3e48f41b17a493b58cb7988d3e211f6b63126c55fe82", + "sha256:e738aabff3586091221044b7a584865ddc4d6120346d12e28e788307cd731043", + "sha256:e7f6173302f8a329ca5d1ee592af9e628d3ade87816e9958dcf7cdae2841def7", + "sha256:e9d036a9a41fc78e8a3f10a86c2fc1098fca8fab8715ba9eb999ce4788d35df0", + "sha256:ea142255d4901b03f89cb6a94411ecec117786a76fc9ab043af8f51dd50b5313", + "sha256:ebd3e6b0c7d4954cca59d241970011f8d3327633d555051c430bd09ff49dc494", + "sha256:ec656680fc53a13f849c71afd0c84a55c536206d524cbc831cde80abbe80489e", + "sha256:ec8df0ff5a911c6d21957a9182402aad7bf060eaeffd77c9ea1c16aecab5adbf", + "sha256:ed95d66745f53e129e935ad726167d3a6cb18c5d33df3165974d54742c373868", + "sha256:ef2c9499b7bd1e24e473dc1a85de55d72fd084eea3d8bdeec7ee0720decb54fa", + "sha256:f5252ba8b43906f206048fa569debf2cd0da0316e8d5b4d25abe53307f573941", + "sha256:f737fef6e117856400afee4f17774cdea392b28ecf058833f5eca368a18cf1bf", + "sha256:fc726c3fa8f606d07bd2b500e5dc4c0fd664c59be7788a16b9e34352c50b6b6b" + ], + "index": "pypi", + "markers": "python_version >= '3.9'", + "version": "==3.11.9" }, "aiormq": { "hashes": [ @@ -166,20 +151,20 @@ }, "alembic": { "hashes": [ - "sha256:203503117415561e203aa14541740643a611f641517f0209fcae63e9fa09f1a2", - "sha256:908e905976d15235fae59c9ac42c4c5b75cfcefe3d27c0fbf7ae15a37715d80e" + "sha256:99bd884ca390466db5e27ffccff1d179ec5c05c965cfefc0607e69f9e411cb25", + "sha256:b00892b53b3642d0b8dbedba234dbf1924b69be83a9a769d5a624b01094e304b" ], "index": "pypi", "markers": "python_version >= '3.8'", - "version": "==1.13.3" + "version": "==1.14.0" }, "anyio": { "hashes": [ - "sha256:4c8bc31ccdb51c7f7bd251f51c609e038d63e34219b44aa86e47576389880b4c", - "sha256:6d170c36fba3bdd840c73d3868c1e777e33676a69c3a72cf0a0d5d6d8009b61d" + "sha256:2f834749c602966b7d456a7567cafcb309f96482b5081d14ac93ccd457f9dd48", + "sha256:ea60c3723ab42ba6fff7e8ccb0488c898ec538ff4df1f1d5e642c3601d07e352" ], "markers": "python_version >= '3.9'", - "version": "==4.6.2.post1" + "version": "==4.7.0" }, "asgi-correlation-id": { "hashes": [ @@ -272,46 +257,44 @@ }, "azure-storage-blob": { "hashes": [ - "sha256:1c2238aa841d1545f42714a5017c010366137a44a0605da2d45f770174bfc6b4", - "sha256:a587e54d4e39d2a27bd75109db164ffa2058fe194061e5446c5a89bca918272f" + "sha256:4f0bb4592ea79a2d986063696514c781c9e62be240f09f6397986e01755bc071", + "sha256:eaaaa1507c8c363d6e1d1342bd549938fdf1adec9b1ada8658c8f5bf3aea844e" ], "index": "pypi", "markers": "python_version >= '3.8'", - "version": "==12.23.1" + "version": "==12.24.0" }, "bcrypt": { "hashes": [ - "sha256:096a15d26ed6ce37a14c1ac1e48119660f21b24cba457f160a4b830f3fe6b5cb", - "sha256:0da52759f7f30e83f1e30a888d9163a81353ef224d82dc58eb5bb52efcabc399", - "sha256:1bb429fedbe0249465cdd85a58e8376f31bb315e484f16e68ca4c786dcc04291", - "sha256:1d84cf6d877918620b687b8fd1bf7781d11e8a0998f576c7aa939776b512b98d", - "sha256:1ee38e858bf5d0287c39b7a1fc59eec64bbf880c7d504d3a06a96c16e14058e7", - "sha256:1ff39b78a52cf03fdf902635e4c81e544714861ba3f0efc56558979dd4f09170", - "sha256:27fe0f57bb5573104b5a6de5e4153c60814c711b29364c10a75a54bb6d7ff48d", - "sha256:3413bd60460f76097ee2e0a493ccebe4a7601918219c02f503984f0a7ee0aebe", - "sha256:3698393a1b1f1fd5714524193849d0c6d524d33523acca37cd28f02899285060", - "sha256:373db9abe198e8e2c70d12b479464e0d5092cc122b20ec504097b5f2297ed184", - "sha256:39e1d30c7233cfc54f5c3f2c825156fe044efdd3e0b9d309512cc514a263ec2a", - "sha256:3bbbfb2734f0e4f37c5136130405332640a1e46e6b23e000eeff2ba8d005da68", - "sha256:3d3a6d28cb2305b43feac298774b997e372e56c7c7afd90a12b3dc49b189151c", - "sha256:5a1e8aa9b28ae28020a3ac4b053117fb51c57a010b9f969603ed885f23841458", - "sha256:61ed14326ee023917ecd093ee6ef422a72f3aec6f07e21ea5f10622b735538a9", - "sha256:655ea221910bcac76ea08aaa76df427ef8625f92e55a8ee44fbf7753dbabb328", - "sha256:762a2c5fb35f89606a9fde5e51392dad0cd1ab7ae64149a8b935fe8d79dd5ed7", - "sha256:77800b7147c9dc905db1cba26abe31e504d8247ac73580b4aa179f98e6608f34", - "sha256:8ac68872c82f1add6a20bd489870c71b00ebacd2e9134a8aa3f98a0052ab4b0e", - "sha256:8d7bb9c42801035e61c109c345a28ed7e84426ae4865511eb82e913df18f58c2", - "sha256:8f6ede91359e5df88d1f5c1ef47428a4420136f3ce97763e31b86dd8280fbdf5", - "sha256:9c1c4ad86351339c5f320ca372dfba6cb6beb25e8efc659bedd918d921956bae", - "sha256:c02d944ca89d9b1922ceb8a46460dd17df1ba37ab66feac4870f6862a1533c00", - "sha256:c52aac18ea1f4a4f65963ea4f9530c306b56ccd0c6f8c8da0c06976e34a6e841", - "sha256:cb2a8ec2bc07d3553ccebf0746bbf3d19426d1c6d1adbd4fa48925f66af7b9e8", - "sha256:cf69eaf5185fd58f268f805b505ce31f9b9fc2d64b376642164e9244540c1221", - "sha256:f4f4acf526fcd1c34e7ce851147deedd4e26e6402369304220250598b26448db" + "sha256:041fa0155c9004eb98a232d54da05c0b41d4b8e66b6fc3cb71b4b3f6144ba837", + "sha256:04e56e3fe8308a88b77e0afd20bec516f74aecf391cdd6e374f15cbed32783d6", + "sha256:1340411a0894b7d3ef562fb233e4b6ed58add185228650942bdc885362f32c17", + "sha256:533e7f3bcf2f07caee7ad98124fab7499cb3333ba2274f7a36cf1daee7409d99", + "sha256:6765386e3ab87f569b276988742039baab087b2cdb01e809d74e74503c2faafe", + "sha256:687cf30e6681eeda39548a93ce9bfbb300e48b4d445a43db4298d2474d2a1e54", + "sha256:76132c176a6d9953cdc83c296aeaed65e1a708485fd55abf163e0d9f8f16ce0e", + "sha256:76d3e352b32f4eeb34703370e370997065d28a561e4a18afe4fef07249cb4396", + "sha256:807261df60a8b1ccd13e6599c779014a362ae4e795f5c59747f60208daddd96d", + "sha256:89df2aea2c43be1e1fa066df5f86c8ce822ab70a30e4c210968669565c0f4685", + "sha256:8ad2f4528cbf0febe80e5a3a57d7a74e6635e41af1ea5675282a33d769fba413", + "sha256:8c458cd103e6c5d1d85cf600e546a639f234964d0228909d8f8dbeebff82d526", + "sha256:8dbd0747208912b1e4ce730c6725cb56c07ac734b3629b60d4398f082ea718ad", + "sha256:909faa1027900f2252a9ca5dfebd25fc0ef1417943824783d1c8418dd7d6df4a", + "sha256:aaa2e285be097050dba798d537b6efd9b698aa88eef52ec98d23dcd6d7cf6fea", + "sha256:adadd36274510a01f33e6dc08f5824b97c9580583bd4487c564fc4617b328005", + "sha256:b1ee315739bc8387aa36ff127afc99120ee452924e0df517a8f3e4c0187a0f5f", + "sha256:b588af02b89d9fad33e5f98f7838bf590d6d692df7153647724a7f20c186f6bf", + "sha256:b7703ede632dc945ed1172d6f24e9f30f27b1b1a067f32f68bf169c5f08d0425", + "sha256:c6f5fa3775966cca251848d4d5393ab016b3afed251163c1436fefdec3b02c84", + "sha256:cde78d385d5e93ece5479a0a87f73cd6fa26b171c786a884f955e165032b262c", + "sha256:cfdf3d7530c790432046c40cda41dfee8c83e29482e6a604f8930b9930e94139", + "sha256:e158009a54c4c8bc91d5e0da80920d048f918c61a581f0a63e4e93bb556d362f", + "sha256:e84e0e6f8e40a242b11bce56c313edc2be121cec3e0ec2d76fce01f6af33c07c", + "sha256:f85b1ffa09240c89aa2e1ae9f3b1c687104f7b2b9d2098da4e923f1b7082d331" ], "index": "pypi", "markers": "python_version >= '3.7'", - "version": "==4.2.0" + "version": "==4.2.1" }, "blinker": { "hashes": [ @@ -323,12 +306,12 @@ }, "boto3": { "hashes": [ - "sha256:0b307f685875e9c7857ce21c0d3050d8d4f3778455a6852d5f98ac75194b400e", - "sha256:65b808e4cf1af8c2f405382d53656a0d92eee8f85c7388c43d64c7a5571b065f" + "sha256:a09871805f8e462349a1c33c23eb413668df0bf68424e61d53518e1a7d883b2f", + "sha256:cc819cdbccbc2d0dc185f1dcfe74cf3809489c4cae63c2e5d6a557aa0c5ab928" ], "index": "pypi", "markers": "python_version >= '3.8'", - "version": "==1.35.47" + "version": "==1.35.77" }, "botocore": { "hashes": [ @@ -441,7 +424,7 @@ "sha256:f7f5baafcc48261359e14bcd6d9bff6d4b28d9103847c9e136694cb0501aef87", "sha256:fc48c783f9c87e60831201f2cce7f3b2e4846bf4d8728eabe54d60700b318a0b" ], - "markers": "platform_python_implementation != 'PyPy'", + "markers": "python_version >= '3.8'", "version": "==1.17.1" }, "charset-normalizer": { @@ -697,12 +680,12 @@ }, "fastapi": { "hashes": [ - "sha256:8035e8f9a2b0aa89cea03b6c77721178ed5358e1aea4cd8570d9466895c0638c", - "sha256:c091c6a35599c036d676fa24bd4a6e19fa30058d93d950216cdc672881f6f7db" + "sha256:9ec46f7addc14ea472958a96aae5b5de65f39721a46aaf5705c480d9a8b76654", + "sha256:e9240b29e36fa8f4bb7290316988e90c381e5092e0cbe84e7818cc3713bcf305" ], "index": "pypi", "markers": "python_version >= '3.8'", - "version": "==0.115.3" + "version": "==0.115.6" }, "fastapi-mail": { "hashes": [ @@ -873,16 +856,16 @@ "sha256:1b2ce6e0b791aee89a1e4f072beba1012247e89baca361eed721fb467fe054b0", "sha256:b49f0019d7bd0d4ab5972a4cff13994b0aabe72d24242200d904db2fb49df7f7" ], - "markers": "platform_python_implementation != 'PyPy'", + "markers": "python_version >= '3.7'", "version": "==2.19.0" }, "google-cloud-storage": { "hashes": [ - "sha256:97a4d45c368b7d401ed48c4fdfe86e1e1cb96401c9e199e419d289e2c0370166", - "sha256:aaf7acd70cdad9f274d29332673fcab98708d0e1f4dceb5a5356aaef06af4d99" + "sha256:aeb971b5c29cf8ab98445082cbfe7b161a1f48ed275822f59ed3f1524ea54fba", + "sha256:cd05e9e7191ba6cb68934d8eb76054d9be4562aa89dbc4236feee4d7d51342b2" ], "markers": "python_version >= '3.7'", - "version": "==2.18.2" + "version": "==2.19.0" }, "google-crc32c": { "hashes": [ @@ -1009,6 +992,7 @@ "sha256:f406b22b7c9a9b4f8aa9d2ab13d6ae0ac3e85c9a809bd590ad53fed2bf70dc79", "sha256:f6ff3b14f2df4c41660a7dec01045a045653998784bf8cfcb5a525bdffffbc8f" ], + "markers": "python_version >= '3.7'", "version": "==3.1.1" }, "grpcio": { @@ -1077,6 +1061,7 @@ "sha256:289bdd7b2459794a12cf95dc0cb727bd4a1742c37bd823f760236c937e53a485", "sha256:f9049b762ba8de6b1086789d8315846e094edac2c50beaf462338b301a8fd4b8" ], + "markers": "python_version >= '3.6'", "version": "==1.62.3" }, "h11": { @@ -1149,16 +1134,17 @@ "sha256:f9eb89ecf8b290f2e293325c646a211ff1c2493222798bb80a530c5e7502494f", "sha256:fc411e1c0a7dcd2f902c7c48cf079947a7e65b5485dea9decb82b9105ca71a43" ], + "markers": "python_full_version >= '3.8.0'", "version": "==0.6.4" }, "httpx": { "hashes": [ - "sha256:7bb2708e112d8fdd7829cd4243970f0c223274051cb35ee80c03301ee29a3df0", - "sha256:f7c2be1d2f3c3c3160d441802406b206c2b76f5947b11115e6df10c6c65e66c2" + "sha256:0858d3bab51ba7e386637f22a61d8ccddaeec5f3fe4209da3a6168dbb91573e0", + "sha256:dc0b419a0cfeb6e8b34e85167c0da2671206f5095f1baa9663d23bcfd6b535fc" ], "index": "pypi", "markers": "python_version >= '3.8'", - "version": "==0.27.2" + "version": "==0.28.0" }, "idna": { "hashes": [ @@ -1203,11 +1189,11 @@ }, "mako": { "hashes": [ - "sha256:9ec3a1583713479fae654f83ed9fa8c9a4c16b7bb0daba0e6bbebff50c0d983d", - "sha256:a91198468092a2f1a0de86ca92690fb0cfc43ca90ee17e15d93662b4c04b241a" + "sha256:42f48953c7eb91332040ff567eb7eea69b22e7a4affbc5ba8e845e8f730f6627", + "sha256:577b97e414580d3e088d47c2dbbe9594aa7a5146ed2875d4dfa9075af2dd3cc8" ], "markers": "python_version >= '3.8'", - "version": "==1.3.6" + "version": "==1.3.8" }, "markdown-it-py": { "hashes": [ @@ -1292,6 +1278,15 @@ "markers": "python_version >= '3.7'", "version": "==0.1.2" }, + "more-itertools": { + "hashes": [ + "sha256:037b0d3203ce90cca8ab1defbbdac29d5f993fc20131f3664dc8d6acfa872aef", + "sha256:5482bfef7849c25dc3c6dd53a6173ae4795da2a41a80faea6700d9f5846c5da6" + ], + "index": "pypi", + "markers": "python_version >= '3.8'", + "version": "==10.5.0" + }, "msgpack": { "hashes": [ "sha256:06f5fd2f6bb2a7914922d935d3b8bb4a7fff3a9a91cfce6d06c13bc42bec975b", @@ -1462,25 +1457,33 @@ }, "nh3": { "hashes": [ - "sha256:0411beb0589eacb6734f28d5497ca2ed379eafab8ad8c84b31bb5c34072b7164", - "sha256:14c5a72e9fe82aea5fe3072116ad4661af5cf8e8ff8fc5ad3450f123e4925e86", - "sha256:19aaba96e0f795bd0a6c56291495ff59364f4300d4a39b29a0abc9cb3774a84b", - "sha256:34c03fa78e328c691f982b7c03d4423bdfd7da69cd707fe572f544cf74ac23ad", - "sha256:36c95d4b70530b320b365659bb5034341316e6a9b30f0b25fa9c9eff4c27a204", - "sha256:3a157ab149e591bb638a55c8c6bcb8cdb559c8b12c13a8affaba6cedfe51713a", - "sha256:42c64511469005058cd17cc1537578eac40ae9f7200bedcfd1fc1a05f4f8c200", - "sha256:5f36b271dae35c465ef5e9090e1fdaba4a60a56f0bb0ba03e0932a66f28b9189", - "sha256:6955369e4d9f48f41e3f238a9e60f9410645db7e07435e62c6a9ea6135a4907f", - "sha256:7b7c2a3c9eb1a827d42539aa64091640bd275b81e097cd1d8d82ef91ffa2e811", - "sha256:8ce0f819d2f1933953fca255db2471ad58184a60508f03e6285e5114b6254844", - "sha256:94a166927e53972a9698af9542ace4e38b9de50c34352b962f4d9a7d4c927af4", - "sha256:a7f1b5b2c15866f2db413a3649a8fe4fd7b428ae58be2c0f6bca5eefd53ca2be", - "sha256:c8b3a1cebcba9b3669ed1a84cc65bf005728d2f0bc1ed2a6594a992e817f3a50", - "sha256:de3ceed6e661954871d6cd78b410213bdcb136f79aafe22aa7182e028b8c7307", - "sha256:f0eca9ca8628dbb4e916ae2491d72957fdd35f7a5d326b7032a345f111ac07fe" - ], - "index": "pypi", - "version": "==0.2.18" + "sha256:00810cd5275f5c3f44b9eb0e521d1a841ee2f8023622de39ffc7d88bd533d8e0", + "sha256:0b6820fc64f2ff7ef3e7253a093c946a87865c877b3889149a6d21d322ed8dbd", + "sha256:11270b16c1b012677e3e2dd166c1aa273388776bf99a3e3677179db5097ee16a", + "sha256:2b926f179eb4bce72b651bfdf76f8aa05d167b2b72bc2f3657fd319f40232adc", + "sha256:2eb021804e9df1761abeb844bb86648d77aa118a663c82f50ea04110d87ed707", + "sha256:3805161c4e12088bd74752ba69630e915bc30fe666034f47217a2f16b16efc37", + "sha256:5d4f5e2189861b352b73acb803b5f4bb409c2f36275d22717e27d4e0c217ae55", + "sha256:75c7cafb840f24430b009f7368945cb5ca88b2b54bb384ebfba495f16bc9c121", + "sha256:790056b54c068ff8dceb443eaefb696b84beff58cca6c07afd754d17692a4804", + "sha256:7e98621856b0a911c21faa5eef8f8ea3e691526c2433f9afc2be713cb6fbdb48", + "sha256:833b3b5f1783ce95834a13030300cea00cbdfd64ea29260d01af9c4821da0aa9", + "sha256:a7b928862daddb29805a1010a0282f77f4b8b238a37b5f76bc6c0d16d930fd22", + "sha256:ac536a4b5c073fdadd8f5f4889adabe1cbdae55305366fb870723c96ca7f49c3", + "sha256:b8eb7affc590e542fa7981ef508cd1644f62176bcd10d4429890fc629b47f0bc", + "sha256:c2e3f0d18cc101132fe10ab7ef5c4f41411297e639e23b64b5e888ccaad63f41", + "sha256:d0adf00e2b2026fa10a42537b60d161e516f206781c7515e4e97e09f72a8c5d0", + "sha256:d53a4577b6123ca1d7e8483fad3e13cb7eda28913d516bd0a648c1a473aa21a9", + "sha256:d8325d51e47cb5b11f649d55e626d56c76041ba508cd59e0cb1cf687cc7612f1", + "sha256:df8eac98fec80bd6f5fd0ae27a65de14f1e1a65a76d8e2237eb695f9cd1121d9", + "sha256:e3dedd7858a21312f7675841529941035a2ac91057db13402c8fe907aa19205a", + "sha256:ec9c8bf86e397cb88c560361f60fdce478b5edb8b93f04ead419b72fbe937ea6", + "sha256:ed06ed78f6b69d57463b46a04f68f270605301e69d80756a8adf7519002de57d", + "sha256:fc483dd8d20f8f8c010783a25a84db3bebeadced92d24d34b40d687f8043ac69", + "sha256:fdb20740d24ab9f2a1341458a00a11205294e97e905de060eeab1ceca020c09c" + ], + "index": "pypi", + "version": "==0.2.19" }, "opentelemetry-api": { "hashes": [ @@ -1911,12 +1914,12 @@ "crypto" ], "hashes": [ - "sha256:3b02fb0f44517787776cf48f2ae25d8e14f300e6d7545a4315cee571a415e850", - "sha256:7e1e5b56cc735432a7369cbfa0efe50fa113ebecdc04ae6922deba8b84582d0c" + "sha256:3cc5772eb20009233caf06e9d8a0577824723b44e6648ee0a2aedb6cf9381953", + "sha256:dcdd193e30abefd5debf142f9adfcdd2b58004e644f25406ffaebd50bd98dacb" ], "index": "pypi", - "markers": "python_version >= '3.8'", - "version": "==2.9.0" + "markers": "python_version >= '3.9'", + "version": "==2.10.1" }, "pymongo": { "hashes": [ @@ -1998,7 +2001,7 @@ "sha256:93d9577b88da0bbea8cc8334ee8b918ed014968fd2ec383e868fb8afb1ccef84", "sha256:cbf74e27246d595d9a74b186b810f6fbb86726dbf3b9532efb343f6d7294fe9c" ], - "markers": "python_version > '3.0'", + "markers": "python_version >= '3.9'", "version": "==3.2.0" }, "python-dateutil": { @@ -2014,16 +2017,17 @@ "sha256:e324ee90a023d808f1959c46bcbc04446a10ced277783dc6ee09987c37ec10ca", "sha256:f7b63ef50f1b690dddf550d03497b66d609393b40b564ed0d674909a68ebf16a" ], + "markers": "python_version >= '3.8'", "version": "==1.0.1" }, "python-multipart": { "hashes": [ - "sha256:045e1f98d719c1ce085ed7f7e1ef9d8ccc8c02ba02b5566d5f7521410ced58cb", - "sha256:43dcf96cf65888a9cd3423544dd0d75ac10f7aa0c3c28a175bbcd00c9ce1aebf" + "sha256:905502ef39050557b7a6af411f454bc19526529ca46ae6831508438890ce12cc", + "sha256:f8d5b0b9c618575bf9df01c684ded1d94a338839bdd8223838afacfb4bb2082d" ], "index": "pypi", "markers": "python_version >= '3.8'", - "version": "==0.0.12" + "version": "==0.0.19" }, "pytz": { "hashes": [ @@ -2088,16 +2092,17 @@ "sha256:f753120cb8181e736c57ef7636e83f31b9c0d1722c516f7e86cf15b7aa57ff12", "sha256:ff3824dc5261f50c9b0dfb3be22b4567a6f938ccce4587b38952d85fd9e9afe4" ], + "markers": "python_version >= '3.8'", "version": "==6.0.2" }, "redis": { "hashes": [ - "sha256:f6c997521fedbae53387307c5d0bf784d9acc28d9f1d058abeac566ec4dbed72", - "sha256:f8ea06b7482a668c6475ae202ed8d9bcaa409f6e87fb77ed1043d912afd62e24" + "sha256:0b1087665a771b1ff2e003aa5bdd354f15a70c9e25d5a7dbf9c722c16528a7b0", + "sha256:ae174f2bb3b1bf2b09d54bf3e51fbc1469cf6c10aa03e21141f51969801a7897" ], "index": "pypi", "markers": "python_version >= '3.8'", - "version": "==5.1.1" + "version": "==5.2.0" }, "requests": { "hashes": [ @@ -2133,6 +2138,7 @@ }, "sentry-sdk": { "hashes": [ + "sha256:7b0b3b709dee051337244a09a30dbf6e95afe0d34a1f8b430d45e0982a7c125b", "sha256:ee4a4d2ae8bfe3cac012dcf3e4607975904c137e1738116549fc3dbbb6ff0e36" ], @@ -2158,11 +2164,11 @@ }, "six": { "hashes": [ - "sha256:1e61c37477a1626458e36f7b1d82aa5c9b094fa4802892072e49de9c60c4c926", - "sha256:8abb2f1d86890a2dfb989f9a77cfcfd3e47c2a354b01111771326f8aa26e0254" + "sha256:4721f391ed90541fddacab5acf947aa0d3dc7d27b2e1e8eda2be8970586c3274", + "sha256:ff70335d468e7eb6ec65b95b99d3a2836546063f63acc5171de367e834932a81" ], "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2'", - "version": "==1.16.0" + "version": "==1.17.0" }, "sniffio": { "hashes": [ @@ -2273,11 +2279,11 @@ }, "taskiq-dependencies": { "hashes": [ - "sha256:04546a5786e0f8cb2e008af19cdf415dd27d63d6d29ccf15eda8fa6b8b6b8006", - "sha256:8b10d2635a8ada8774f1b555e0a6d72c4fb5e6089601858d38dd95ff6d214a4c" + "sha256:5756690fd9f1f9efac34aac5959f173724e898a852de790238f97090467c4e3c", + "sha256:fba4dbaac47dfc8817fd2efeb13cefe41f45790b4871ba65fcdfd3c74f7121dc" ], - "markers": "python_full_version >= '3.8.1' and python_full_version < '4.0.0'", - "version": "==1.5.4" + "markers": "python_version >= '3.9' and python_version < '4.0'", + "version": "==1.5.6" }, "taskiq-fastapi": { "hashes": [ @@ -2299,12 +2305,12 @@ }, "typer": { "hashes": [ - "sha256:62fe4e471711b147e3365034133904df3e235698399bc4de2b36c8579298d52b", - "sha256:f592f089bedcc8ec1b974125d64851029c3b1af145f04aca64d69410f0c9b722" + "sha256:7994fb7b8155b64d3402518560648446072864beefd44aa2dc36972a5972e847", + "sha256:a0588c0a7fa68a1978a069818657778f86abe6ff5ea6abf472f940a08bfe4f0a" ], "index": "pypi", "markers": "python_version >= '3.7'", - "version": "==0.12.5" + "version": "==0.15.1" }, "typing-extensions": { "hashes": [ @@ -2335,11 +2341,11 @@ "standard" ], "hashes": [ - "sha256:60b8f3a5ac027dcd31448f411ced12b5ef452c646f76f02f8cc3f25d8d26fd82", - "sha256:f78b36b143c16f54ccdb8190d0a26b5f1901fe5a3c777e1ab29f26391af8551e" + "sha256:82ad92fd58da0d12af7482ecdb5f2470a04c9c9a53ced65b9bbb4a205377602e", + "sha256:ee9519c246a72b1c084cea8d3b44ed6026e78a4a309cbedae9c37e4cb9fbb175" ], "markers": "python_version >= '3.8'", - "version": "==0.32.0" + "version": "==0.32.1" }, "uvloop": { "hashes": [ @@ -2381,6 +2387,7 @@ "sha256:f3df876acd7ec037a3d005b3ab85a7e4110422e4d9c1571d4fc89b0fc41b6816", "sha256:f7089d2dc73179ce5ac255bdf37c236a9f914b264825fdaacaded6990a7fb4c2" ], + "markers": "python_full_version >= '3.8.0'", "version": "==0.21.0" }, "watchdog": { @@ -2414,6 +2421,7 @@ "sha256:ea5d86d1bcf4a9d24610aa2f6f25492f441960cf04aed2bd9a97db439b643a7b", "sha256:efe3252137392a471a2174d721e1037a0e6a5da7beb72a021e662b7000a9903f" ], + "markers": "python_version >= '3.6'", "version": "==2.3.1" }, "watchfiles": { @@ -2644,6 +2652,7 @@ ], "markers": "python_version >= '3.6'", "version": "==0.14.2" + }, "yarl": { "hashes": [ @@ -3004,13 +3013,14 @@ ], "markers": "python_version >= '3.9'", "version": "==7.6.8" + }, "decorator": { "hashes": [ "sha256:637996211036b6385ef91435e4fae22989472f9d571faba8927ba8253acbc330", "sha256:b8c3f85900b9dc423225913c5aace94729fe1fa9763b38939a95226f02d37186" ], - "markers": "python_version >= '3.11'", + "markers": "python_version >= '3.5'", "version": "==5.1.1" }, "distlib": { @@ -3220,6 +3230,7 @@ "sha256:f406b22b7c9a9b4f8aa9d2ab13d6ae0ac3e85c9a809bd590ad53fed2bf70dc79", "sha256:f6ff3b14f2df4c41660a7dec01045a045653998784bf8cfcb5a525bdffffbc8f" ], + "markers": "python_version >= '3.7'", "version": "==3.1.1" }, "html5rdf": { @@ -3275,7 +3286,9 @@ "sha256:85ec56a7e20f6c38fce7727dcca699ae4ffc85985aa7b23635a8008f918ae321", "sha256:cb0a405a306d2995a5cbb9901894d240784a9f341394c6ba3f4fe8c6eb89ff6e" ], + "markers": "python_version >= '3.11'", + "version": "==8.30.0" }, "jedi": { @@ -3680,7 +3693,7 @@ "sha256:93d9577b88da0bbea8cc8334ee8b918ed014968fd2ec383e868fb8afb1ccef84", "sha256:cbf74e27246d595d9a74b186b810f6fbb86726dbf3b9532efb343f6d7294fe9c" ], - "markers": "python_version > '3.0'", + "markers": "python_version >= '3.9'", "version": "==3.2.0" }, "pyshacl": { @@ -3693,12 +3706,12 @@ }, "pytest": { "hashes": [ - "sha256:70b98107bd648308a7952b06e6ca9a50bc660be218d53c257cc1fc94fda10181", - "sha256:a6853c7375b2663155079443d2e45de913a911a11d669df02a50814944db57b2" + "sha256:50e16d954148559c9a74109af1eaf0c945ba2d8f30f0a3d3335edde19788b6f6", + "sha256:965370d062bce11e73868e0335abac31b4d3de0e82f4007408d242b4f8610761" ], "index": "pypi", "markers": "python_version >= '3.8'", - "version": "==8.3.3" + "version": "==8.3.4" }, "pytest-asyncio": { "hashes": [ @@ -3711,12 +3724,12 @@ }, "pytest-cov": { "hashes": [ - "sha256:4f0764a1219df53214206bf1feea4633c3b558a2925c8b59f144f682861ce652", - "sha256:5837b58e9f6ebd335b0f8060eecce69b662415b16dc503883a02f45dfeb14857" + "sha256:eee6f1b9e61008bd34975a4d5bab25801eb31898b032dd55addc93e96fcaaa35", + "sha256:fde0b595ca248bb8e2d76f020b465f3b107c9632e6a1d1705f17834c89dcadc0" ], "index": "pypi", - "markers": "python_version >= '3.8'", - "version": "==5.0.0" + "markers": "python_version >= '3.9'", + "version": "==6.0.0" }, "pytest-env": { "hashes": [ @@ -3809,6 +3822,7 @@ "sha256:f753120cb8181e736c57ef7636e83f31b9c0d1722c516f7e86cf15b7aa57ff12", "sha256:ff3824dc5261f50c9b0dfb3be22b4567a6f938ccce4587b38952d85fd9e9afe4" ], + "markers": "python_version >= '3.8'", "version": "==6.0.2" }, "rdflib": { @@ -3848,28 +3862,28 @@ }, "ruff": { "hashes": [ - "sha256:0cdf20c2b6ff98e37df47b2b0bd3a34aaa155f59a11182c1303cce79be715628", - "sha256:10842f69c245e78d6adec7e1db0a7d9ddc2fff0621d730e61657b64fa36f207e", - "sha256:194d6c46c98c73949a106425ed40a576f52291c12bc21399eb8f13a0f7073495", - "sha256:1eb54986f770f49edb14f71d33312d79e00e629a57387382200b1ef12d6a4ef9", - "sha256:211d877674e9373d4bb0f1c80f97a0201c61bcd1e9d045b6e9726adc42c156aa", - "sha256:214b88498684e20b6b2b8852c01d50f0651f3cc6118dfa113b4def9f14faaf06", - "sha256:47a86360cf62d9cd53ebfb0b5eb0e882193fc191c6d717e8bef4462bc3b9ea2b", - "sha256:496494d350c7fdeb36ca4ef1c9f21d80d182423718782222c29b3e72b3512737", - "sha256:4b406c2dce5be9bad59f2de26139a86017a517e6bcd2688da515481c05a2cb11", - "sha256:630fce3fefe9844e91ea5bbf7ceadab4f9981f42b704fae011bb8efcaf5d84be", - "sha256:82c2579b82b9973a110fab281860403b397c08c403de92de19568f32f7178598", - "sha256:9af971fe85dcd5eaed8f585ddbc6bdbe8c217fb8fcf510ea6bca5bdfff56040e", - "sha256:ab7d98c7eed355166f367597e513a6c82408df4181a937628dbec79abb2a1fe4", - "sha256:b641c7f16939b7d24b7bfc0be4102c56562a18281f84f635604e8a6989948914", - "sha256:d71672336e46b34e0c90a790afeac8a31954fd42872c1f6adaea1dff76fd44f9", - "sha256:dc452ba6f2bb9cf8726a84aa877061a2462afe9ae0ea1d411c53d226661c601d", - "sha256:f6c968509f767776f524a8430426539587d5ec5c662f6addb6aa25bc2e8195ec", - "sha256:ff4aabfbaaba880e85d394603b9e75d32b0693152e16fa659a3064a85df7fce2" + "sha256:1ca4e3a87496dc07d2427b7dd7ffa88a1e597c28dad65ae6433ecb9f2e4f022f", + "sha256:2aae99ec70abf43372612a838d97bfe77d45146254568d94926e8ed5bbb409ea", + "sha256:32096b41aaf7a5cc095fa45b4167b890e4c8d3fd217603f3634c92a541de7248", + "sha256:5fe716592ae8a376c2673fdfc1f5c0c193a6d0411f90a496863c99cd9e2ae25d", + "sha256:60f578c11feb1d3d257b2fb043ddb47501ab4816e7e221fbb0077f0d5d4e7b6f", + "sha256:705832cd7d85605cb7858d8a13d75993c8f3ef1397b0831289109e953d833d29", + "sha256:729850feed82ef2440aa27946ab39c18cb4a8889c1128a6d589ffa028ddcfc22", + "sha256:81c148825277e737493242b44c5388a300584d73d5774defa9245aaef55448b0", + "sha256:ac42caaa0411d6a7d9594363294416e0e48fc1279e1b0e948391695db2b3d5b1", + "sha256:b402ddee3d777683de60ff76da801fa7e5e8a71038f57ee53e903afbcefdaa58", + "sha256:b84f4f414dda8ac7f75075c1fa0b905ac0ff25361f42e6d5da681a465e0f78e5", + "sha256:c49ab4da37e7c457105aadfd2725e24305ff9bc908487a9bf8d548c6dad8bb3d", + "sha256:cbd5cf9b0ae8f30eebc7b360171bd50f59ab29d39f06a670b3e4501a36ba5897", + "sha256:d261d7850c8367704874847d95febc698a950bf061c9475d4a8b7689adc4f7fa", + "sha256:e769083da9439508833cfc7c23e351e1809e67f47c50248250ce1ac52c21fb93", + "sha256:ec016beb69ac16be416c435828be702ee694c0d722505f9c1f35e1b9c0cc1bf5", + "sha256:f05cdf8d050b30e2ba55c9b09330b51f9f97d36d4673213679b965d25a785f3c", + "sha256:fb88e2a506b70cfbc2de6fae6681c4f944f7dd5f2fe87233a7233d888bad73e8" ], "index": "pypi", "markers": "python_version >= '3.7'", - "version": "==0.7.0" + "version": "==0.8.2" }, "setuptools": { "hashes": [ @@ -3881,11 +3895,11 @@ }, "six": { "hashes": [ - "sha256:1e61c37477a1626458e36f7b1d82aa5c9b094fa4802892072e49de9c60c4c926", - "sha256:8abb2f1d86890a2dfb989f9a77cfcfd3e47c2a354b01111771326f8aa26e0254" + "sha256:4721f391ed90541fddacab5acf947aa0d3dc7d27b2e1e8eda2be8970586c3274", + "sha256:ff70335d468e7eb6ec65b95b99d3a2836546063f63acc5171de367e834932a81" ], "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2'", - "version": "==1.16.0" + "version": "==1.17.0" }, "stack-data": { "hashes": [ diff --git a/src/apps/activities/api/activities.py b/src/apps/activities/api/activities.py index 193de76c11e..3b326ce7617 100644 --- a/src/apps/activities/api/activities.py +++ b/src/apps/activities/api/activities.py @@ -5,14 +5,17 @@ from apps.activities.crud import ActivitiesCRUD from apps.activities.domain.activity import ( + ActivitiesMetadata, ActivityLanguageWithItemsMobileDetailPublic, ActivityOrFlowWithAssignmentsPublic, ActivitySingleLanguageWithItemsDetailPublic, + ActivitySubjectMetadata, ActivityWithAssignmentDetailsPublic, ) from apps.activities.filters import AppletActivityFilter from apps.activities.services.activity import ActivityItemService, ActivityService from apps.activity_assignments.service import ActivityAssignmentService +from apps.activity_flows.crud import FlowsCRUD from apps.activity_flows.domain.flow import FlowWithAssignmentDetailsPublic from apps.activity_flows.service.flow import FlowService from apps.answers.deps.preprocess_arbitrary import get_answer_session @@ -169,7 +172,8 @@ async def applet_activities_for_target_subject( applet_service = AppletService(session, user.id) await applet_service.exist_by_id(applet_id) - await SubjectsService(session, user.id).exist_by_id(subject_id) + subject = await SubjectsService(session, user.id).exist_by_id(subject_id) + is_limited_respondent = subject.user_id is None # Restrict the endpoint access to owners, managers, coordinators, and assigned reviewers await CheckAccessService(session, user.id).check_subject_subject_access(applet_id, subject_id) @@ -191,7 +195,7 @@ async def applet_activities_for_target_subject( activities_and_flows = await ActivityService(session, user.id).get_activity_and_flow_basic_info_by_ids_or_auto( applet_id=applet_id, ids=activity_and_flow_ids_from_submissions + activity_and_flow_ids_from_assignments, - include_auto=True, + include_auto=not is_limited_respondent, language=language, ) @@ -358,3 +362,118 @@ async def __filter_activities( activities.remove(activity) return activities + + +async def applet_activities_metadata_for_subject( + applet_id: uuid.UUID, + subject_id: uuid.UUID, + user: User = Depends(get_current_user), + language: str = Depends(get_language), + session=Depends(get_session), + answer_session=Depends(get_answer_session), +) -> Response[ActivitiesMetadata]: + applet_service = AppletService(session, user.id) + await applet_service.exist_by_id(applet_id) + await CheckAccessService(session, user.id).check_applet_detail_access(applet_id) + + subject = await SubjectsService(session, user.id).exist_by_id(subject_id) + is_limited_respondent = subject.user_id is None + + # Fetch assigned activity or flow IDs for the subject + assigned_activities = await ActivityAssignmentService(session).get_assigned_activity_or_flow_ids_for_subject( + subject_id + ) + + # Fetch activities submissions by the subject + submissions_metadata = await AnswerService(session, user.id, answer_session).get_submissions_metadata_by_subject( + subject_id + ) + + # Fetch auto-assigned activity and flow IDs by applet ID + auto_activity_ids = await ActivityService(session, user.id).get_activity_and_flow_ids_by_applet_id_auto(applet_id) + + # Combine all assigned IDs and submitted activity IDs + all_activity_ids = ( + set(assigned_activities.activities.keys()) + | set(submissions_metadata.activities.keys()) + | set(auto_activity_ids) + ) + + activities = await ActivitiesCRUD(session).get_by_applet_id_and_activities_ids(applet_id, list(all_activity_ids)) + flows = await FlowsCRUD(session).get_by_applet_id_and_flows_ids(applet_id, list(all_activity_ids)) + + activities_state = {activity.id: activity.soft_exists() for activity in activities} + flows_state = {flow.id: flow.soft_exists() for flow in flows} + + # Initialize ActivitiesCounters with zero counts + activities_metadata = ActivitiesMetadata(subject_id=subject_id) + + # Iterate over all activity or flow IDs + for activity_or_flow_id in all_activity_ids: + is_auto = activity_or_flow_id in auto_activity_ids + + # Get submission and assignment data if available + submission_data = submissions_metadata.activities.get(activity_or_flow_id) + assignments_data = assigned_activities.activities.get(activity_or_flow_id) + + # Initialize sets for respondents and subjects + respondents = set() + subjects = set() + + # Initialize submission counts + respondent_submissions_count = 0 + respondent_last_submission_date = None + subject_submissions_count = 0 + subject_last_submission_date = None + + # Update from submission data + if submission_data: + respondents.update(submission_data.respondents) + subjects.update(submission_data.subjects) + respondent_submissions_count = submission_data.respondent_submissions_count + respondent_last_submission_date = submission_data.respondent_last_submission_date + subject_submissions_count = submission_data.subject_submissions_count + subject_last_submission_date = submission_data.subject_last_submission_date + + # Update from assignment data + if assignments_data: + respondents.update(assignments_data.respondents) + subjects.update(assignments_data.subjects) + + # Include the subject for auto-assigned activities, excluding limited accounts + if is_auto and not is_limited_respondent: + respondents.add(subject_id) + subjects.add(subject_id) + + # Calculate counts + respondents_count = len(respondents) + subjects_count = len(subjects) + + activity_or_flow_exists = activities_state.get(activity_or_flow_id) or flows_state.get(activity_or_flow_id) + + # Update activities counters counts + if subjects_count > 0: + if activity_or_flow_exists: + activities_metadata.respondent_activities_count_existing += 1 + else: + activities_metadata.respondent_activities_count_deleted += 1 + if respondents_count > 0: + if activity_or_flow_exists: + activities_metadata.target_activities_count_existing += 1 + else: + activities_metadata.target_activities_count_deleted += 1 + + # Append the activity subject counters + activities_metadata.activities_or_flows.append( + ActivitySubjectMetadata( + activity_or_flow_id=activity_or_flow_id, + respondents_count=respondents_count, + subjects_count=subjects_count, + respondent_submissions_count=respondent_submissions_count, + respondent_last_submission_date=respondent_last_submission_date, + subject_submissions_count=subject_submissions_count, + subject_last_submission_date=subject_last_submission_date, + ) + ) + + return Response(result=activities_metadata) diff --git a/src/apps/activities/crud/activity.py b/src/apps/activities/crud/activity.py index 6061c5681c8..10345553e9e 100644 --- a/src/apps/activities/crud/activity.py +++ b/src/apps/activities/crud/activity.py @@ -107,6 +107,12 @@ async def get_ids_by_applet_id(self, applet_id: uuid.UUID) -> list[uuid.UUID]: result = await self._execute(query) return result.scalars().all() + async def get_ids_by_applet_id_auto(self, applet_id: uuid.UUID) -> list[uuid.UUID]: + query: Query = select(ActivitySchema.id) + query = query.where(ActivitySchema.applet_id == applet_id, ActivitySchema.auto_assign.is_(True)) + result = await self._execute(query) + return result.scalars().all() + async def get_activity_and_flow_basic_info_by_ids_or_auto( self, applet_id: uuid.UUID, ids: list[uuid.UUID], include_auto: bool, language: str ) -> list[ActivityOrFlowBasicInfoInternal]: diff --git a/src/apps/activities/domain/activity.py b/src/apps/activities/domain/activity.py index 8305d5e5e1f..b0413e69467 100644 --- a/src/apps/activities/domain/activity.py +++ b/src/apps/activities/domain/activity.py @@ -156,3 +156,22 @@ class ActivityBaseInfo(ActivityMinimumInfo, InternalModel): contains_response_types: list[ResponseType] item_count: int auto_assign: bool + + +class ActivitySubjectMetadata(PublicModel): + activity_or_flow_id: uuid.UUID + respondents_count: int + respondent_submissions_count: int + respondent_last_submission_date: datetime | None + subjects_count: int + subject_submissions_count: int + subject_last_submission_date: datetime | None + + +class ActivitiesMetadata(PublicModel): + subject_id: uuid.UUID + respondent_activities_count_existing: int = 0 + respondent_activities_count_deleted: int = 0 + target_activities_count_existing: int = 0 + target_activities_count_deleted: int = 0 + activities_or_flows: list[ActivitySubjectMetadata] = Field(default_factory=list) diff --git a/src/apps/activities/router.py b/src/apps/activities/router.py index c54145866dc..46184c7fef7 100644 --- a/src/apps/activities/router.py +++ b/src/apps/activities/router.py @@ -8,10 +8,12 @@ applet_activities_for_respondent_subject, applet_activities_for_subject, applet_activities_for_target_subject, + applet_activities_metadata_for_subject, public_activity_retrieve, ) from apps.activities.api.reusable_item_choices import item_choice_create, item_choice_delete, item_choice_retrieve from apps.activities.domain.activity import ( + ActivitiesMetadata, ActivityOrFlowWithAssignmentsPublic, ActivitySingleLanguageWithItemsDetailPublic, ) @@ -133,3 +135,16 @@ **DEFAULT_OPENAPI_RESPONSE, }, )(applet_activities_for_respondent_subject) + +router.get( + "/applet/{applet_id}/subject/{subject_id}/metadata", + description="""Get metadata, like number of submissions and participants for activities and activity + flows associated with the given participant, either as respondent or target subject. + """, + status_code=status.HTTP_200_OK, + responses={ + status.HTTP_200_OK: {"model": Response[ActivitiesMetadata]}, + **AUTHENTICATION_ERROR_RESPONSES, + **DEFAULT_OPENAPI_RESPONSE, + }, +)(applet_activities_metadata_for_subject) diff --git a/src/apps/activities/services/activity.py b/src/apps/activities/services/activity.py index b10d46527fa..31af33ed9a6 100644 --- a/src/apps/activities/services/activity.py +++ b/src/apps/activities/services/activity.py @@ -1,3 +1,4 @@ +import asyncio import uuid from apps.activities.crud import ActivitiesCRUD, ActivityHistoriesCRUD @@ -20,6 +21,7 @@ from apps.activities.errors import ActivityAccessDeniedError, ActivityDoeNotExist from apps.activities.services.activity_item import ActivityItemService from apps.activity_assignments.service import ActivityAssignmentService +from apps.activity_flows.crud import FlowsCRUD from apps.applets.crud import AppletsCRUD, UserAppletAccessCRUD from apps.schedule.crud.events import ActivityEventsCRUD, EventCRUD from apps.schedule.service.schedule import ScheduleService @@ -462,3 +464,11 @@ async def get_activity_and_flow_basic_info_by_ids_or_auto( return await ActivitiesCRUD(self.session).get_activity_and_flow_basic_info_by_ids_or_auto( applet_id, ids, include_auto, language ) + + async def get_activity_and_flow_ids_by_applet_id_auto(self, applet_id: uuid.UUID) -> list[uuid.UUID]: + activity_ids_coro = ActivitiesCRUD(self.session).get_ids_by_applet_id_auto(applet_id) + flow_ids_coro = FlowsCRUD(self.session).get_ids_by_applet_id_auto(applet_id) + + activity_ids, flow_ids = await asyncio.gather(activity_ids_coro, flow_ids_coro) + + return activity_ids + flow_ids diff --git a/src/apps/activities/tests/test_activities.py b/src/apps/activities/tests/test_activities.py index 43faa1f8352..f558c5bc066 100644 --- a/src/apps/activities/tests/test_activities.py +++ b/src/apps/activities/tests/test_activities.py @@ -32,6 +32,7 @@ from apps.shared.test.client import TestClient from apps.subjects.db.schemas import SubjectSchema from apps.subjects.domain import Subject +from apps.subjects.services import SubjectsService from apps.themes.domain import Theme from apps.users.domain import User from apps.workspaces.domain.constants import Role @@ -283,6 +284,7 @@ class TestActivities: subject_assigned_activities_url = "/activities/applet/{applet_id}/subject/{subject_id}" target_assigned_activities_url = "/activities/applet/{applet_id}/target/{subject_id}" respondent_assigned_activities_url = "/activities/applet/{applet_id}/respondent/{subject_id}" + activity_metadata_by_subject_url = "/activities/applet/{applet_id}/subject/{subject_id}/metadata" async def test_activity_detail(self, client: TestClient, applet_one: AppletFull, tom: User): activity = applet_one.activities[0] @@ -1266,6 +1268,26 @@ async def test_assigned_activities_limited_respondent( assert result_activity["performanceTaskType"] is None assert len(result_activity["assignments"]) == 0 + response = await client.get( + self.activity_metadata_by_subject_url.format( + applet_id=applet_one.id, subject_id=applet_one_shell_account.id + ) + ) + + assert response.status_code == http.HTTPStatus.OK + result = response.json()["result"] + + assert result["respondentActivitiesCountExisting"] == 1 + assert result["respondentActivitiesCountDeleted"] == 0 + assert result["targetActivitiesCountExisting"] == 0 + assert result["targetActivitiesCountDeleted"] == 0 + assert len(result["activitiesOrFlows"]) == 1 + assert result["activitiesOrFlows"][0]["activityOrFlowId"] == str(activity.id) + assert result["activitiesOrFlows"][0]["respondentsCount"] == 0 + assert result["activitiesOrFlows"][0]["respondentSubmissionsCount"] == 1 + assert result["activitiesOrFlows"][0]["subjectsCount"] == 1 + assert result["activitiesOrFlows"][0]["subjectSubmissionsCount"] == 0 + @pytest.mark.parametrize("subject_type", ["target", "respondent"]) async def test_assigned_activities_auto_assigned( self, @@ -1317,6 +1339,37 @@ async def test_assigned_activities_auto_assigned( assert activity_result["isPerformanceTask"] is False assert activity_result["performanceTaskType"] is None + response = await client.get( + self.activity_metadata_by_subject_url.format( + applet_id=applet_activity_flow_lucy_manager.id, subject_id=lucy_applet_activity_flow_subject.id + ) + ) + + assert response.status_code == http.HTTPStatus.OK + result = response.json()["result"] + + assert result["respondentActivitiesCountExisting"] == 2 + assert result["respondentActivitiesCountDeleted"] == 0 + assert result["targetActivitiesCountExisting"] == 2 + assert result["targetActivitiesCountDeleted"] == 0 + assert len(result["activitiesOrFlows"]) == 2 + flow_metadata = next(item for item in result["activitiesOrFlows"] if item["activityOrFlowId"] == str(flow.id)) + activity_metadata = next( + item for item in result["activitiesOrFlows"] if item["activityOrFlowId"] == str(activity.id) + ) + assert flow_metadata["respondentsCount"] == 1 + assert flow_metadata["respondentSubmissionsCount"] == 0 + assert flow_metadata["respondentLastSubmissionDate"] is None + assert flow_metadata["subjectsCount"] == 1 + assert flow_metadata["subjectSubmissionsCount"] == 0 + assert flow_metadata["subjectLastSubmissionDate"] is None + assert activity_metadata["respondentsCount"] == 1 + assert activity_metadata["respondentSubmissionsCount"] == 0 + assert activity_metadata["respondentLastSubmissionDate"] is None + assert activity_metadata["subjectsCount"] == 1 + assert activity_metadata["subjectSubmissionsCount"] == 0 + assert activity_metadata["subjectLastSubmissionDate"] is None + @pytest.mark.parametrize( "subject_type,result_order", [ @@ -1567,6 +1620,60 @@ async def test_assigned_activities_manually_assigned( subject_attr = "targetSubject" if subject_type == "target" else "respondentSubject" assert flow_assignment[subject_attr]["id"] == str(user_empty_applet_subject.id) + as_respondent = ( + {activity_by_number[1].id, activity_by_number[2].id} + if subject_type == "target" + else {activity_by_number[4].id, activity_by_number[3].id} + ) + as_target = ( + {activity_by_number[1].id, activity_by_number[3].id} + if subject_type == "target" + else {activity_by_number[4].id, activity_by_number[2].id} + ) + + as_respondent.update( + {flow_by_number[1].id, flow_by_number[2].id} + if subject_type == "target" + else { + flow_by_number[4].id, + flow_by_number[3].id, + } + ) + as_target.update( + {flow_by_number[1].id, flow_by_number[3].id} + if subject_type == "target" + else { + flow_by_number[4].id, + flow_by_number[2].id, + } + ) + + client.login(lucy) + + response = await client.get( + self.activity_metadata_by_subject_url.format( + applet_id=empty_applet_lucy_manager.id, + subject_id=user_empty_applet_subject.id if subject_type == "target" else lucy_empty_applet_subject.id, + ) + ) + + assert response.status_code == http.HTTPStatus.OK + result = response.json()["result"] + + assert len(result["activitiesOrFlows"]) == 6 + assert result["respondentActivitiesCountExisting"] == 4 + assert result["respondentActivitiesCountDeleted"] == 0 + assert result["targetActivitiesCountExisting"] == 4 + assert result["targetActivitiesCountDeleted"] == 0 + for activityOrFlow in result["activitiesOrFlows"]: + is_respondent = uuid.UUID(activityOrFlow["activityOrFlowId"]) in as_respondent + is_target = uuid.UUID(activityOrFlow["activityOrFlowId"]) in as_target + + assert activityOrFlow["respondentsCount"] == (1 if is_target else 0) + assert activityOrFlow["respondentSubmissionsCount"] == 0 + assert activityOrFlow["subjectsCount"] == (1 if is_respondent else 0) + assert activityOrFlow["subjectSubmissionsCount"] == 0 + @pytest.mark.parametrize("subject_type", ["target", "respondent"]) async def test_assigned_activities_from_submission( self, @@ -1634,6 +1741,36 @@ async def test_assigned_activities_from_submission( assert result_activity["performanceTaskType"] is None assert len(result_activity["assignments"]) == 0 + response = await client.get( + self.activity_metadata_by_subject_url.format( + applet_id=applet_one_lucy_respondent.id, subject_id=tom_applet_one_subject.id + ) + ) + + assert response.status_code == http.HTTPStatus.OK + result = response.json()["result"] + + assert result["respondentActivitiesCountExisting"] == (0 if subject_type == "target" else 1) + assert result["respondentActivitiesCountDeleted"] == 0 + assert result["targetActivitiesCountExisting"] == (1 if subject_type == "target" else 0) + assert result["targetActivitiesCountDeleted"] == 0 + assert len(result["activitiesOrFlows"]) == 1 + activityOrFlow = result["activitiesOrFlows"][0] + assert activityOrFlow["respondentsCount"] == (1 if subject_type == "target" else 0) + assert activityOrFlow["respondentSubmissionsCount"] == (0 if subject_type == "target" else 1) + assert ( + activityOrFlow["respondentLastSubmissionDate"] is None + if subject_type == "target" + else activityOrFlow["respondentLastSubmissionDate"] is not None + ) + assert activityOrFlow["subjectsCount"] == (0 if subject_type == "target" else 1) + assert activityOrFlow["subjectSubmissionsCount"] == (1 if subject_type == "target" else 0) + assert ( + activityOrFlow["subjectLastSubmissionDate"] is not None + if subject_type == "target" + else activityOrFlow["subjectLastSubmissionDate"] is None + ) + @pytest.mark.parametrize("subject_type", ["target", "respondent"]) async def test_assigned_hidden_activities( self, @@ -1784,6 +1921,31 @@ async def test_assigned_hidden_activities( assert result_activity_auto["performanceTaskType"] is None assert len(result_activity_auto["assignments"]) == 0 + response = await client.get( + self.activity_metadata_by_subject_url.format( + applet_id=empty_applet_lucy_manager.id, + subject_id=user_empty_applet_subject.id if subject_type == "target" else lucy_empty_applet_subject.id, + ) + ) + + assert response.status_code == http.HTTPStatus.OK + result = response.json()["result"] + + assert len(result["activitiesOrFlows"]) == 4 + assert result["respondentActivitiesCountExisting"] == (2 if subject_type == "target" else 4) + assert result["respondentActivitiesCountDeleted"] == 0 + assert result["targetActivitiesCountExisting"] == (4 if subject_type == "target" else 2) + assert result["targetActivitiesCountDeleted"] == 0 + for activityOrFlow in result["activitiesOrFlows"]: + is_auto = activityOrFlow["activityOrFlowId"] == str(auto_flow.id) or activityOrFlow[ + "activityOrFlowId" + ] == str(auto_activity.id) + + assert activityOrFlow["respondentsCount"] == (1 if is_auto or subject_type == "target" else 0) + assert activityOrFlow["respondentSubmissionsCount"] == 0 + assert activityOrFlow["subjectsCount"] == (1 if is_auto or subject_type != "target" else 0) + assert activityOrFlow["subjectSubmissionsCount"] == 0 + @pytest.mark.parametrize("subject_type", ["target", "respondent"]) async def test_assigned_performance_tasks( self, @@ -1816,3 +1978,91 @@ async def test_assigned_performance_tasks( PerformanceTaskType.ABTRAILS.value, PerformanceTaskType.UNITY.value, ] + + async def test_activities_metadata_deleted_subject( + self, + session: AsyncSession, + client: TestClient, + lucy: User, + empty_applet_lucy_manager: AppletFull, + lucy_empty_applet_subject: Subject, + user_empty_applet_subject: Subject, + activity_create_session: ActivityCreate, + ): + activity_service = ActivityService(session, lucy.id) + activities = await activity_service.update_create( + empty_applet_lucy_manager.id, + [ + ActivityUpdate( + **activity_create_session.dict(exclude={"name", "auto_assign"}), + name="Manual Activity 1", + auto_assign=False, + ), + ActivityUpdate( + **activity_create_session.dict(exclude={"name", "auto_assign"}), + name="Manual Activity 2", + auto_assign=False, + ), + ], + ) + + await ActivityAssignmentService(session).create_many( + empty_applet_lucy_manager.id, + [ + ActivityAssignmentCreate( + activity_id=activities[0].id, + activity_flow_id=None, + respondent_subject_id=lucy_empty_applet_subject.id, + target_subject_id=user_empty_applet_subject.id, + ), + ActivityAssignmentCreate( + activity_id=activities[1].id, + activity_flow_id=None, + respondent_subject_id=user_empty_applet_subject.id, + target_subject_id=user_empty_applet_subject.id, + ), + ], + ) + + manual_activity_2 = next((activity for activity in activities if activity.name == "Manual Activity 2")) + + client.login(lucy) + + response = await client.get( + self.activity_metadata_by_subject_url.format( + applet_id=empty_applet_lucy_manager.id, subject_id=user_empty_applet_subject.id + ) + ) + + assert response.status_code == http.HTTPStatus.OK + result = response.json()["result"] + + assert result["respondentActivitiesCountExisting"] == 1 + assert result["respondentActivitiesCountDeleted"] == 0 + assert result["targetActivitiesCountExisting"] == 2 + assert result["targetActivitiesCountDeleted"] == 0 + assert len(result["activitiesOrFlows"]) == 2 + for activityOrFlow in result["activitiesOrFlows"]: + assert activityOrFlow["respondentsCount"] == 1 + assert activityOrFlow["respondentSubmissionsCount"] == 0 + assert activityOrFlow["subjectsCount"] == ( + 1 if activityOrFlow["activityOrFlowId"] == str(manual_activity_2.id) else 0 + ) + assert activityOrFlow["subjectSubmissionsCount"] == 0 + + await SubjectsService(session, lucy.id).delete(user_empty_applet_subject.id) + + response = await client.get( + self.activity_metadata_by_subject_url.format( + applet_id=empty_applet_lucy_manager.id, subject_id=user_empty_applet_subject.id + ) + ) + + assert response.status_code == http.HTTPStatus.OK + result = response.json()["result"] + + assert result["respondentActivitiesCountExisting"] == 0 + assert result["respondentActivitiesCountDeleted"] == 0 + assert result["targetActivitiesCountExisting"] == 0 + assert result["targetActivitiesCountDeleted"] == 0 + assert len(result["activitiesOrFlows"]) == 0 diff --git a/src/apps/activity_assignments/crud/assignments.py b/src/apps/activity_assignments/crud/assignments.py index 1ab2f9ac69c..1ee90e151c1 100644 --- a/src/apps/activity_assignments/crud/assignments.py +++ b/src/apps/activity_assignments/crud/assignments.py @@ -1,9 +1,9 @@ import datetime import uuid -from sqlalchemy import and_, or_, select, tuple_, update +from sqlalchemy import and_, case, func, or_, select, tuple_, update from sqlalchemy.dialects.postgresql import insert -from sqlalchemy.orm import Query, aliased +from sqlalchemy.orm import InstrumentedAttribute, Query, aliased from apps.activities.db.schemas import ActivitySchema from apps.activity_assignments.db.schemas import ActivityAssigmentSchema @@ -315,3 +315,55 @@ async def check_if_auto_assigned(self, activity_or_flow_id: uuid.UUID) -> bool | db_result = await self._execute(union_query) return db_result.scalar_one_or_none() + + @staticmethod + def _activity_and_flow_ids_by_subject_query(subject_column: InstrumentedAttribute, subject_id: uuid.UUID) -> Query: + respondent_schema = aliased(SubjectSchema) + target_schema = aliased(SubjectSchema) + + query: Query = ( + select( + case( + ( + ActivityAssigmentSchema.activity_id.isnot(None), + ActivityAssigmentSchema.activity_id, + ), + else_=ActivityAssigmentSchema.activity_flow_id, + ).label("activity_id"), + func.array_agg( + ActivityAssigmentSchema.respondent_subject_id + if subject_column == ActivityAssigmentSchema.target_subject_id + else ActivityAssigmentSchema.target_subject_id + ).label("subject_ids"), + func.count(ActivityAssigmentSchema.id).label("assignments_count"), + ) + .join(respondent_schema, respondent_schema.id == ActivityAssigmentSchema.respondent_subject_id) + .join(target_schema, target_schema.id == ActivityAssigmentSchema.target_subject_id) + .where( + subject_column == subject_id, + ActivityAssigmentSchema.soft_exists(), + respondent_schema.soft_exists(), + target_schema.soft_exists(), + ) + .group_by("activity_id", ActivityAssigmentSchema.activity_id, ActivityAssigmentSchema.activity_flow_id) + ) + + return query + + async def get_assignments_by_target_subject(self, target_subject_id: uuid.UUID) -> list[dict]: + query: Query = self._activity_and_flow_ids_by_subject_query( + ActivityAssigmentSchema.target_subject_id, target_subject_id + ) + + res = await self._execute(query) + + return res.mappings().all() + + async def get_assignments_by_respondent_subject(self, respondent_subject_id: uuid.UUID) -> list[dict]: + query: Query = self._activity_and_flow_ids_by_subject_query( + ActivityAssigmentSchema.respondent_subject_id, respondent_subject_id + ) + + res = await self._execute(query) + + return res.mappings().all() diff --git a/src/apps/activity_assignments/domain/assignments.py b/src/apps/activity_assignments/domain/assignments.py index 91a313a46a3..ca9ea6f1b2b 100644 --- a/src/apps/activity_assignments/domain/assignments.py +++ b/src/apps/activity_assignments/domain/assignments.py @@ -1,6 +1,7 @@ +import uuid from uuid import UUID -from pydantic import BaseModel, root_validator +from pydantic import BaseModel, Field, root_validator from apps.activity_assignments.errors import ( ActivityAssignmentActivityOrFlowError, @@ -79,3 +80,15 @@ def validate_assignments(cls, values): class ActivitiesAssignmentsDelete(InternalModel): assignments: list[ActivityAssignmentDelete] + + +class AssignmentsSubjectCounters(PublicModel): + respondents: set[uuid.UUID] = Field(default_factory=set) + subjects: set[uuid.UUID] = Field(default_factory=set) + subject_assignments_count: int = 0 + respondent_assignments_count: int = 0 + + +class AssignmentsActivityCountBySubject(PublicModel): + subject_id: uuid.UUID + activities: dict[uuid.UUID, AssignmentsSubjectCounters] = Field(default_factory=dict) diff --git a/src/apps/activity_assignments/service.py b/src/apps/activity_assignments/service.py index 9ac2a5bfce4..cde77bc5687 100644 --- a/src/apps/activity_assignments/service.py +++ b/src/apps/activity_assignments/service.py @@ -11,6 +11,8 @@ ActivityAssignmentCreate, ActivityAssignmentDelete, ActivityAssignmentWithSubject, + AssignmentsActivityCountBySubject, + AssignmentsSubjectCounters, ) from apps.activity_flows.crud import FlowsCRUD from apps.activity_flows.db.schemas import ActivityFlowSchema @@ -447,6 +449,36 @@ async def check_if_auto_assigned(self, activity_or_flow_id: uuid.UUID) -> bool | """ return await ActivityAssigmentCRUD(self.session).check_if_auto_assigned(activity_or_flow_id) + async def get_assigned_activity_or_flow_ids_for_subject( + self, subject_id: uuid.UUID + ) -> AssignmentsActivityCountBySubject: + assignments_target_coro = ActivityAssigmentCRUD(self.session).get_assignments_by_target_subject(subject_id) + assignments_respondent_coro = ActivityAssigmentCRUD(self.session).get_assignments_by_respondent_subject( + subject_id + ) + + assignments_target, assignments_respondent = await asyncio.gather( + assignments_target_coro, assignments_respondent_coro + ) + + assignments_activity_count = AssignmentsActivityCountBySubject(subject_id=subject_id) + + for activityOrFlow in assignments_target: + activity_counters = assignments_activity_count.activities.setdefault( + activityOrFlow["activity_id"], AssignmentsSubjectCounters() + ) + activity_counters.subject_assignments_count = activityOrFlow["assignments_count"] + activity_counters.respondents.update(activityOrFlow["subject_ids"]) + + for activityOrFlow in assignments_respondent: + activity_counters = assignments_activity_count.activities.setdefault( + activityOrFlow["activity_id"], AssignmentsSubjectCounters() + ) + activity_counters.respondent_assignments_count = activityOrFlow["assignments_count"] + activity_counters.subjects.update(activityOrFlow["subject_ids"]) + + return assignments_activity_count + @staticmethod def _get_email_template_name() -> str: return "new_activity_assignments" diff --git a/src/apps/activity_flows/crud/flow.py b/src/apps/activity_flows/crud/flow.py index 6b8171fcf5c..0b1ae41dc2e 100644 --- a/src/apps/activity_flows/crud/flow.py +++ b/src/apps/activity_flows/crud/flow.py @@ -67,3 +67,10 @@ async def get_ids_by_applet_id(self, applet_id: uuid.UUID) -> list[uuid.UUID]: result = await self._execute(query) return result.scalars().all() + + async def get_ids_by_applet_id_auto(self, applet_id: uuid.UUID) -> list[uuid.UUID]: + query: Query = select(ActivityFlowSchema.id) + query = query.where(ActivityFlowSchema.applet_id == applet_id, ActivityFlowSchema.auto_assign.is_(True)) + + result = await self._execute(query) + return result.scalars().all() diff --git a/src/apps/answers/crud/answers.py b/src/apps/answers/crud/answers.py index fb5ef8c3a9c..38f439e3a10 100644 --- a/src/apps/answers/crud/answers.py +++ b/src/apps/answers/crud/answers.py @@ -942,7 +942,7 @@ async def delete_by_ids(self, ids: list[uuid.UUID]): async def get_target_subject_ids_by_respondent( self, respondent_subject_id: uuid.UUID, activity_or_flow_id: uuid.UUID - ): + ) -> list[tuple[uuid.UUID, int]]: query: Query = ( select( AnswerSchema.target_subject_id, @@ -950,10 +950,15 @@ async def get_target_subject_ids_by_respondent( ) .where( AnswerSchema.source_subject_id == respondent_subject_id, - or_( - AnswerSchema.id_from_history_id(AnswerSchema.activity_history_id) == str(activity_or_flow_id), - AnswerSchema.id_from_history_id(AnswerSchema.flow_history_id) == str(activity_or_flow_id), + case( + ( + AnswerSchema.flow_history_id.isnot(None), + AnswerSchema.id_from_history_id(AnswerSchema.flow_history_id) == str(activity_or_flow_id), + ), + else_=AnswerSchema.id_from_history_id(AnswerSchema.activity_history_id) == str(activity_or_flow_id), ), + # Exclude incomplete activity flow assessments + AnswerSchema.is_flow_completed.isnot(False), ) .group_by(AnswerSchema.target_subject_id) ) @@ -971,27 +976,81 @@ def __activity_and_flow_ids_by_subject_query(subject_column: InstrumentedAttribu AnswerSchema.id_from_history_id(AnswerSchema.flow_history_id), ), else_=AnswerSchema.id_from_history_id(AnswerSchema.activity_history_id), - ).label("id") + ).label("activity_id"), + ( + AnswerSchema.source_subject_id + if subject_column == AnswerSchema.target_subject_id + else AnswerSchema.target_subject_id + ).label("subject_id"), ) - .where(subject_column == subject_id) - .distinct() + .where( + subject_column == subject_id, + # Exclude incomplete activity flow assessments + AnswerSchema.is_flow_completed.isnot(False), + ) + .group_by("activity_id", "subject_id") ) return query - async def get_activity_and_flow_ids_by_target_subject(self, target_subject_id: uuid.UUID) -> list[uuid.UUID]: + async def get_activity_and_flow_ids_by_target_subject(self, target_subject_id: uuid.UUID) -> list[dict]: """ Get a list of activity and flow IDs based on answers submitted for a target subject """ res = await self._execute( self.__activity_and_flow_ids_by_subject_query(AnswerSchema.target_subject_id, target_subject_id) ) - return res.scalars().all() + return res.mappings().all() - async def get_activity_and_flow_ids_by_source_subject(self, source_subject_id: uuid.UUID) -> list[uuid.UUID]: + async def get_activity_and_flow_ids_by_source_subject(self, source_subject_id: uuid.UUID) -> list[dict]: """ Get a list of activity and flow IDs based on answers submitted for a source subject """ res = await self._execute( self.__activity_and_flow_ids_by_subject_query(AnswerSchema.source_subject_id, source_subject_id) ) - return res.scalars().all() + return res.mappings().all() + + @staticmethod + def _query_submissions_metadata_by_subject(subject_column: InstrumentedAttribute, subject_id: uuid.UUID) -> Query: + query: Query = ( + select( + func.count(func.distinct(AnswerSchema.submit_id)).label("submission_count"), + case( + ( + AnswerSchema.flow_history_id.isnot(None), + AnswerSchema.id_from_history_id(AnswerSchema.flow_history_id), + ), + else_=AnswerSchema.id_from_history_id(AnswerSchema.activity_history_id), + ).label("activity_id"), + ( + AnswerSchema.source_subject_id + if subject_column == AnswerSchema.target_subject_id + else AnswerSchema.target_subject_id + ).label("subject_id"), + func.max(AnswerSchema.created_at).label("last_submission_date"), + ) + .where( + subject_column == subject_id, + # Exclude incomplete activity flow assessments + AnswerSchema.is_flow_completed.isnot(False), + ) + .group_by("activity_id", "subject_id") + ) + + return query + + async def get_submissions_metadata_by_target_subject(self, target_subject_id: uuid.UUID) -> list[dict]: + query: Query = self._query_submissions_metadata_by_subject(AnswerSchema.target_subject_id, target_subject_id) + + res = await self._execute(query) + + return res.mappings().all() + + async def get_submissions_metadata_by_respondent_subject(self, respondent_subject_id: uuid.UUID) -> list[dict]: + query: Query = self._query_submissions_metadata_by_subject( + AnswerSchema.source_subject_id, respondent_subject_id + ) + + res = await self._execute(query) + + return res.mappings().all() diff --git a/src/apps/answers/domain/answers.py b/src/apps/answers/domain/answers.py index c3a18bfb169..2072a991f71 100644 --- a/src/apps/answers/domain/answers.py +++ b/src/apps/answers/domain/answers.py @@ -713,3 +713,17 @@ class FilesCopyCheckResult(InternalModel): total_files: int not_copied_files: set[str] files_to_remove: set[str] + + +class SubmissionsSubjectCounters(InternalModel): + respondents: set[uuid.UUID] = Field(default_factory=set) + subjects: set[uuid.UUID] = Field(default_factory=set) + subject_submissions_count: int = 0 + subject_last_submission_date: datetime.datetime | None = None + respondent_submissions_count: int = 0 + respondent_last_submission_date: datetime.datetime | None = None + + +class SubmissionsActivityMetadataBySubject(InternalModel): + subject_id: uuid.UUID + activities: dict[uuid.UUID, SubmissionsSubjectCounters] = Field(default_factory=dict) diff --git a/src/apps/answers/service.py b/src/apps/answers/service.py index 06002d9a7fe..6c6bb2adf2a 100644 --- a/src/apps/answers/service.py +++ b/src/apps/answers/service.py @@ -67,6 +67,8 @@ AppletSubmission, FilesCopyCheckResult, RespondentAnswerData, + SubmissionsActivityMetadataBySubject, + SubmissionsSubjectCounters, ) from apps.answers.errors import ( ActivityIsNotAssessment, @@ -102,6 +104,7 @@ from apps.subjects.crud import SubjectsCrud from apps.subjects.db.schemas import SubjectSchema from apps.subjects.domain import SubjectReadResponse +from apps.subjects.services import SubjectsService from apps.users import User, UserSchema, UsersCRUD from apps.workspaces.crud.applet_access import AppletAccessCRUD from apps.workspaces.crud.user_applet_access import UserAppletAccessCRUD @@ -1938,21 +1941,93 @@ async def get_target_subject_ids_by_respondent_and_activity_or_flow( async def get_activity_and_flow_ids_by_target_subject(self, target_subject_id: uuid.UUID) -> list[uuid.UUID]: """ - Get a list of activity and flow IDs based on answers submitted for a target subject + Get a list of activity and flow IDs based on answers submitted for a target subject. + Excludes answers whose source subject was soft-deleted. The data returned is just a combined list of activity and flow IDs, without any - distinction between the two + distinction between the two. """ - return await AnswersCRUD(self.answer_session).get_activity_and_flow_ids_by_target_subject(target_subject_id) + results = await AnswersCRUD(self.answer_session).get_activity_and_flow_ids_by_target_subject(target_subject_id) + existing_subject_ids = await self._filter_out_soft_deleted_subjects(results) + activity_ids = [result["activity_id"] for result in results if result["subject_id"] in existing_subject_ids] + + return activity_ids async def get_activity_and_flow_ids_by_source_subject(self, source_subject_id: uuid.UUID) -> list[uuid.UUID]: """ - Get a list of activity and flow IDs based on answers submitted for a source subject + Get a list of activity and flow IDs based on answers submitted for a source subject. + Excludes answers whose target subject was soft-deleted. The data returned is just a combined list of activity and flow IDs, without any - distinction between the two + distinction between the two. + """ + results = await AnswersCRUD(self.answer_session).get_activity_and_flow_ids_by_source_subject(source_subject_id) + existing_subject_ids = await self._filter_out_soft_deleted_subjects(results) + activity_ids = [result["activity_id"] for result in results if result["subject_id"] in existing_subject_ids] + + return activity_ids + + async def _filter_out_soft_deleted_subjects(self, submissions: list[dict]) -> set[uuid.UUID]: + """ + Filter out submissions whose subject_id column corresponds to soft-deleted subjects """ - return await AnswersCRUD(self.answer_session).get_activity_and_flow_ids_by_source_subject(source_subject_id) + subject_ids = set([activityOrFlow["subject_id"] for activityOrFlow in submissions]) + + assert self.user_id + existing_subjects = await SubjectsService(self.session, self.user_id).get_by_ids(list(subject_ids)) + existing_subject_ids = {subject.id for subject in existing_subjects} + + return existing_subject_ids + + async def get_submissions_metadata_by_subject(self, subject_id: uuid.UUID) -> SubmissionsActivityMetadataBySubject: + submissions_target_coro = AnswersCRUD(self.answer_session).get_submissions_metadata_by_target_subject( + subject_id + ) + submissions_respondent_coro = AnswersCRUD(self.answer_session).get_submissions_metadata_by_respondent_subject( + subject_id + ) + + submissions_target, submissions_respondent = await asyncio.gather( + submissions_target_coro, submissions_respondent_coro + ) + + existing_subject_ids = await self._filter_out_soft_deleted_subjects(submissions_target + submissions_respondent) + + submissions_activity_metadata = SubmissionsActivityMetadataBySubject(subject_id=subject_id) + + for activity_submissions in submissions_target: + activity_metadata = submissions_activity_metadata.activities.setdefault( + uuid.UUID(activity_submissions["activity_id"]), SubmissionsSubjectCounters() + ) + respondent_subject_id = activity_submissions["subject_id"] + if respondent_subject_id in existing_subject_ids: + activity_metadata.respondents.add(respondent_subject_id) + activity_metadata.subject_submissions_count += activity_submissions["submission_count"] + activity_metadata.subject_last_submission_date = ( + activity_submissions["last_submission_date"] + if not activity_metadata.subject_last_submission_date + else max( + activity_metadata.subject_last_submission_date, activity_submissions["last_submission_date"] + ) + ) + + for activity_submissions in submissions_respondent: + activity_metadata = submissions_activity_metadata.activities.setdefault( + uuid.UUID(activity_submissions["activity_id"]), SubmissionsSubjectCounters() + ) + target_subject_id = activity_submissions["subject_id"] + if target_subject_id in existing_subject_ids: + activity_metadata.subjects.add(target_subject_id) + activity_metadata.respondent_submissions_count += activity_submissions["submission_count"] + activity_metadata.respondent_last_submission_date = ( + activity_submissions["last_submission_date"] + if not activity_metadata.respondent_last_submission_date + else max( + activity_metadata.respondent_last_submission_date, activity_submissions["last_submission_date"] + ) + ) + + return submissions_activity_metadata class ReportServerService: diff --git a/src/apps/authentication/api/auth.py b/src/apps/authentication/api/auth.py index b220b8e6777..a7997e1c2e1 100644 --- a/src/apps/authentication/api/auth.py +++ b/src/apps/authentication/api/auth.py @@ -1,5 +1,5 @@ import uuid -from datetime import datetime +from datetime import datetime, timezone import jwt from fastapi import Body, Depends @@ -86,10 +86,9 @@ async def refresh_access_token( # check transition key transition_key = settings.authentication.refresh_token.transition_key transition_expire_date = settings.authentication.refresh_token.transition_expire_date + today = datetime.now(timezone.utc).date() - if not ( - transition_key and transition_expire_date and transition_expire_date > datetime.utcnow().date() - ): + if not (transition_key and transition_expire_date and transition_expire_date > today): raise payload = jwt.decode( schema.refresh_token, diff --git a/src/apps/authentication/tests/test_auth.py b/src/apps/authentication/tests/test_auth.py index 4464aff35d3..5e34ce216dc 100644 --- a/src/apps/authentication/tests/test_auth.py +++ b/src/apps/authentication/tests/test_auth.py @@ -109,7 +109,7 @@ async def test_refresh_token_key_transition(self, client, tom: User, tom_create: token_data = TokenPayload(**payload) new_token_key = "new token key" - transition_expire_date = datetime.date.today() + datetime.timedelta(days=1) + transition_expire_date = datetime.datetime.now(datetime.timezone.utc).date() + datetime.timedelta(days=1) # refresh access token, check refresh token not changed _status_code, _token = await self._request_refresh_token(client, refresh_token) @@ -129,7 +129,7 @@ async def test_refresh_token_key_transition(self, client, tom: User, tom_create: # check transition expire date with mock.patch("apps.authentication.api.auth.datetime") as date_mock: - date_mock.utcnow().date.return_value = transition_expire_date + datetime.timedelta(days=1) + date_mock.now().date.return_value = transition_expire_date + datetime.timedelta(days=1) _status_code, _ = await self._request_refresh_token(client, refresh_token) assert _status_code == http.HTTPStatus.BAD_REQUEST