diff --git a/pdm.lock b/pdm.lock index 37b56e3c7d..7e5c358795 100644 --- a/pdm.lock +++ b/pdm.lock @@ -5,7 +5,7 @@ groups = ["default", "dev"] strategy = ["inherit_metadata"] lock_version = "4.5.0" -content_hash = "sha256:0f22381b6d4265ae1d7aa2189d5e0f68ebf14481a3f03f51fa73d43bc9608d2a" +content_hash = "sha256:210fde0732f2782e252f1ccb307390eaf0065b121597e381ebaf498b1ba24d63" [[metadata.targets]] requires_python = ">=3.12" @@ -218,13 +218,13 @@ files = [ [[package]] name = "cachetools" -version = "5.3.3" +version = "5.5.0" requires_python = ">=3.7" summary = "Extensible memoizing collections and decorators" groups = ["dev"] files = [ - {file = "cachetools-5.3.3-py3-none-any.whl", hash = "sha256:0abad1021d3f8325b2fc1d2e9c8b9c9d57b04c3932657a72465447332c24d945"}, - {file = "cachetools-5.3.3.tar.gz", hash = "sha256:ba29e2dfa0b8b556606f097407ed1aa62080ee108ab0dc5ec9d6a723a007d105"}, + {file = "cachetools-5.5.0-py3-none-any.whl", hash = "sha256:02134e8439cdc2ffb62023ce1debca2944c3f289d66bb17ead3ab3dede74b292"}, + {file = "cachetools-5.5.0.tar.gz", hash = "sha256:2cc24fb4cbe39633fb7badd9db9ca6295d766d9c2995f245725a46715d050f2a"}, ] [[package]] @@ -434,23 +434,42 @@ files = [ [[package]] name = "coverage" -version = "7.5.1" -requires_python = ">=3.8" +version = "7.6.4" +requires_python = ">=3.9" summary = "Code coverage measurement for Python" groups = ["dev"] files = [ - {file = "coverage-7.5.1-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:b6cf3764c030e5338e7f61f95bd21147963cf6aa16e09d2f74f1fa52013c1206"}, - {file = "coverage-7.5.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:2ec92012fefebee89a6b9c79bc39051a6cb3891d562b9270ab10ecfdadbc0c34"}, - {file = "coverage-7.5.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:16db7f26000a07efcf6aea00316f6ac57e7d9a96501e990a36f40c965ec7a95d"}, - {file = "coverage-7.5.1-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:beccf7b8a10b09c4ae543582c1319c6df47d78fd732f854ac68d518ee1fb97fa"}, - {file = "coverage-7.5.1-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8748731ad392d736cc9ccac03c9845b13bb07d020a33423fa5b3a36521ac6e4e"}, - {file = "coverage-7.5.1-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:7352b9161b33fd0b643ccd1f21f3a3908daaddf414f1c6cb9d3a2fd618bf2572"}, - {file = "coverage-7.5.1-cp312-cp312-musllinux_1_1_i686.whl", hash = "sha256:7a588d39e0925f6a2bff87154752481273cdb1736270642aeb3635cb9b4cad07"}, - {file = "coverage-7.5.1-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:68f962d9b72ce69ea8621f57551b2fa9c70509af757ee3b8105d4f51b92b41a7"}, - {file = "coverage-7.5.1-cp312-cp312-win32.whl", hash = "sha256:f152cbf5b88aaeb836127d920dd0f5e7edff5a66f10c079157306c4343d86c19"}, - {file = "coverage-7.5.1-cp312-cp312-win_amd64.whl", hash = "sha256:5a5740d1fb60ddf268a3811bcd353de34eb56dc24e8f52a7f05ee513b2d4f596"}, - {file = "coverage-7.5.1-pp38.pp39.pp310-none-any.whl", hash = "sha256:6537e7c10cc47c595828b8a8be04c72144725c383c4702703ff4e42e44577312"}, - {file = "coverage-7.5.1.tar.gz", hash = "sha256:54de9ef3a9da981f7af93eafde4ede199e0846cd819eb27c88e2b712aae9708c"}, + {file = "coverage-7.6.4-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:12394842a3a8affa3ba62b0d4ab7e9e210c5e366fbac3e8b2a68636fb19892c2"}, + {file = "coverage-7.6.4-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:2b6b4c83d8e8ea79f27ab80778c19bc037759aea298da4b56621f4474ffeb117"}, + {file = "coverage-7.6.4-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1d5b8007f81b88696d06f7df0cb9af0d3b835fe0c8dbf489bad70b45f0e45613"}, + {file = "coverage-7.6.4-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:b57b768feb866f44eeed9f46975f3d6406380275c5ddfe22f531a2bf187eda27"}, + {file = "coverage-7.6.4-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5915fcdec0e54ee229926868e9b08586376cae1f5faa9bbaf8faf3561b393d52"}, + {file = "coverage-7.6.4-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:0b58c672d14f16ed92a48db984612f5ce3836ae7d72cdd161001cc54512571f2"}, + {file = "coverage-7.6.4-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:2fdef0d83a2d08d69b1f2210a93c416d54e14d9eb398f6ab2f0a209433db19e1"}, + {file = "coverage-7.6.4-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:8cf717ee42012be8c0cb205dbbf18ffa9003c4cbf4ad078db47b95e10748eec5"}, + {file = "coverage-7.6.4-cp312-cp312-win32.whl", hash = "sha256:7bb92c539a624cf86296dd0c68cd5cc286c9eef2d0c3b8b192b604ce9de20a17"}, + {file = "coverage-7.6.4-cp312-cp312-win_amd64.whl", hash = "sha256:1032e178b76a4e2b5b32e19d0fd0abbce4b58e77a1ca695820d10e491fa32b08"}, + {file = "coverage-7.6.4-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:023bf8ee3ec6d35af9c1c6ccc1d18fa69afa1cb29eaac57cb064dbb262a517f9"}, + {file = "coverage-7.6.4-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:b0ac3d42cb51c4b12df9c5f0dd2f13a4f24f01943627120ec4d293c9181219ba"}, + {file = "coverage-7.6.4-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f8fe4984b431f8621ca53d9380901f62bfb54ff759a1348cd140490ada7b693c"}, + {file = "coverage-7.6.4-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:5fbd612f8a091954a0c8dd4c0b571b973487277d26476f8480bfa4b2a65b5d06"}, + {file = "coverage-7.6.4-cp313-cp313-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:dacbc52de979f2823a819571f2e3a350a7e36b8cb7484cdb1e289bceaf35305f"}, + {file = "coverage-7.6.4-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:dab4d16dfef34b185032580e2f2f89253d302facba093d5fa9dbe04f569c4f4b"}, + {file = "coverage-7.6.4-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:862264b12ebb65ad8d863d51f17758b1684560b66ab02770d4f0baf2ff75da21"}, + {file = "coverage-7.6.4-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:5beb1ee382ad32afe424097de57134175fea3faf847b9af002cc7895be4e2a5a"}, + {file = "coverage-7.6.4-cp313-cp313-win32.whl", hash = "sha256:bf20494da9653f6410213424f5f8ad0ed885e01f7e8e59811f572bdb20b8972e"}, + {file = "coverage-7.6.4-cp313-cp313-win_amd64.whl", hash = "sha256:182e6cd5c040cec0a1c8d415a87b67ed01193ed9ad458ee427741c7d8513d963"}, + {file = "coverage-7.6.4-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:a181e99301a0ae128493a24cfe5cfb5b488c4e0bf2f8702091473d033494d04f"}, + {file = "coverage-7.6.4-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:df57bdbeffe694e7842092c5e2e0bc80fff7f43379d465f932ef36f027179806"}, + {file = "coverage-7.6.4-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0bcd1069e710600e8e4cf27f65c90c7843fa8edfb4520fb0ccb88894cad08b11"}, + {file = "coverage-7.6.4-cp313-cp313t-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:99b41d18e6b2a48ba949418db48159d7a2e81c5cc290fc934b7d2380515bd0e3"}, + {file = "coverage-7.6.4-cp313-cp313t-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a6b1e54712ba3474f34b7ef7a41e65bd9037ad47916ccb1cc78769bae324c01a"}, + {file = "coverage-7.6.4-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:53d202fd109416ce011578f321460795abfe10bb901b883cafd9b3ef851bacfc"}, + {file = "coverage-7.6.4-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:c48167910a8f644671de9f2083a23630fbf7a1cb70ce939440cd3328e0919f70"}, + {file = "coverage-7.6.4-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:cc8ff50b50ce532de2fa7a7daae9dd12f0a699bfcd47f20945364e5c31799fef"}, + {file = "coverage-7.6.4-cp313-cp313t-win32.whl", hash = "sha256:b8d3a03d9bfcaf5b0141d07a88456bb6a4c3ce55c080712fec8418ef3610230e"}, + {file = "coverage-7.6.4-cp313-cp313t-win_amd64.whl", hash = "sha256:f3ddf056d3ebcf6ce47bdaf56142af51bb7fad09e4af310241e9db7a3a8022e1"}, + {file = "coverage-7.6.4.tar.gz", hash = "sha256:29fc0f17b1d3fea332f8001d4558f8214af7f1d87a345f3a133c901d60347c73"}, ] [[package]] @@ -496,14 +515,6 @@ files = [ {file = "cryptography-42.0.7-cp39-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:1a58839984d9cb34c855197043eaae2c187d930ca6d644612843b4fe8513c886"}, {file = "cryptography-42.0.7-cp39-abi3-win32.whl", hash = "sha256:e6b79d0adb01aae87e8a44c2b64bc3f3fe59515280e00fb6d57a7267a2583cda"}, {file = "cryptography-42.0.7-cp39-abi3-win_amd64.whl", hash = "sha256:16268d46086bb8ad5bf0a2b5544d8a9ed87a0e33f5e77dd3c3301e63d941a83b"}, - {file = "cryptography-42.0.7-pp310-pypy310_pp73-macosx_10_12_x86_64.whl", hash = "sha256:2954fccea107026512b15afb4aa664a5640cd0af630e2ee3962f2602693f0c82"}, - {file = "cryptography-42.0.7-pp310-pypy310_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:362e7197754c231797ec45ee081f3088a27a47c6c01eff2ac83f60f85a50fe60"}, - {file = "cryptography-42.0.7-pp310-pypy310_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:4f698edacf9c9e0371112792558d2f705b5645076cc0aaae02f816a0171770fd"}, - {file = "cryptography-42.0.7-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:5482e789294854c28237bba77c4c83be698be740e31a3ae5e879ee5444166582"}, - {file = "cryptography-42.0.7-pp39-pypy39_pp73-macosx_10_12_x86_64.whl", hash = "sha256:e9b2a6309f14c0497f348d08a065d52f3020656f675819fc405fb63bbcd26562"}, - {file = "cryptography-42.0.7-pp39-pypy39_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:d8e3098721b84392ee45af2dd554c947c32cc52f862b6a3ae982dbb90f577f14"}, - {file = "cryptography-42.0.7-pp39-pypy39_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:c65f96dad14f8528a447414125e1fc8feb2ad5a272b8f68477abbcc1ea7d94b9"}, - {file = "cryptography-42.0.7-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:36017400817987670037fbb0324d71489b6ead6231c9604f8fc1f7d008087c68"}, {file = "cryptography-42.0.7.tar.gz", hash = "sha256:ecbfbc00bf55888edda9868a4cf927205de8499e7fabe6c050322298382953f2"}, ] @@ -1171,17 +1182,16 @@ files = [ [[package]] name = "factory-boy" -version = "3.3.0" -requires_python = ">=3.7" +version = "3.3.1" +requires_python = ">=3.8" summary = "A versatile test fixtures replacement based on thoughtbot's factory_bot for Ruby." groups = ["dev"] dependencies = [ "Faker>=0.7.0", - "importlib-metadata; python_version < \"3.8\"", ] files = [ - {file = "factory_boy-3.3.0-py2.py3-none-any.whl", hash = "sha256:a2cdbdb63228177aa4f1c52f4b6d83fab2b8623bf602c7dedd7eb83c0f69c04c"}, - {file = "factory_boy-3.3.0.tar.gz", hash = "sha256:bc76d97d1a65bbd9842a6d722882098eb549ec8ee1081f9fb2e8ff29f0c300f1"}, + {file = "factory_boy-3.3.1-py2.py3-none-any.whl", hash = "sha256:7b1113c49736e1e9995bc2a18f4dbf2c52cf0f841103517010b1d825712ce3ca"}, + {file = "factory_boy-3.3.1.tar.gz", hash = "sha256:8317aa5289cdfc45f9cae570feb07a6177316c82e34d14df3c2e1f22f26abef0"}, ] [[package]] @@ -1214,29 +1224,29 @@ files = [ [[package]] name = "filelock" -version = "3.14.0" +version = "3.16.1" requires_python = ">=3.8" summary = "A platform independent file lock." groups = ["dev"] files = [ - {file = "filelock-3.14.0-py3-none-any.whl", hash = "sha256:43339835842f110ca7ae60f1e1c160714c5a6afd15a2873419ab185334975c0f"}, - {file = "filelock-3.14.0.tar.gz", hash = "sha256:6ea72da3be9b8c82afd3edcf99f2fffbb5076335a5ae4d03248bb5b6c3eae78a"}, + {file = "filelock-3.16.1-py3-none-any.whl", hash = "sha256:2082e5703d51fbf98ea75855d9d5527e33d8ff23099bec374a134febee6946b0"}, + {file = "filelock-3.16.1.tar.gz", hash = "sha256:c249fbfcd5db47e5e2d6d62198e565475ee65e4831e2561c8e313fa7eb961435"}, ] [[package]] name = "flake8" -version = "7.0.0" +version = "7.1.1" requires_python = ">=3.8.1" summary = "the modular source code checker: pep8 pyflakes and co" groups = ["dev"] dependencies = [ "mccabe<0.8.0,>=0.7.0", - "pycodestyle<2.12.0,>=2.11.0", + "pycodestyle<2.13.0,>=2.12.0", "pyflakes<3.3.0,>=3.2.0", ] files = [ - {file = "flake8-7.0.0-py2.py3-none-any.whl", hash = "sha256:a6dfbb75e03252917f2473ea9653f7cd799c3064e54d4c8140044c5c065f53c3"}, - {file = "flake8-7.0.0.tar.gz", hash = "sha256:33f96621059e65eec474169085dc92bf26e7b2d47366b70be2f67ab80dc25132"}, + {file = "flake8-7.1.1-py2.py3-none-any.whl", hash = "sha256:597477df7860daa5aa0fdd84bf5208a043ab96b8e96ab708770ae0364dd03213"}, + {file = "flake8-7.1.1.tar.gz", hash = "sha256:049d058491e228e03e67b390f311bbf88fce2dbaa8fa673e7aea87b7198b8d38"}, ] [[package]] @@ -1357,7 +1367,7 @@ files = [ [[package]] name = "ipython" -version = "8.24.0" +version = "8.29.0" requires_python = ">=3.10" summary = "IPython: Productive Interactive Computing" groups = ["dev"] @@ -1375,8 +1385,8 @@ dependencies = [ "typing-extensions>=4.6; python_version < \"3.12\"", ] files = [ - {file = "ipython-8.24.0-py3-none-any.whl", hash = "sha256:d7bf2f6c4314984e3e02393213bab8703cf163ede39672ce5918c51fe253a2a3"}, - {file = "ipython-8.24.0.tar.gz", hash = "sha256:010db3f8a728a578bb641fdd06c063b9fb8e96a9464c63aec6310fbcb5e80501"}, + {file = "ipython-8.29.0-py3-none-any.whl", hash = "sha256:0188a1bd83267192123ccea7f4a8ed0a78910535dbaa3f37671dca76ebd429c8"}, + {file = "ipython-8.29.0.tar.gz", hash = "sha256:40b60e15b22591450eef73e40a027cf77bd652e757523eebc5bd7c7c498290eb"}, ] [[package]] @@ -1443,7 +1453,7 @@ files = [ [[package]] name = "jsonschema" -version = "4.22.0" +version = "4.23.0" requires_python = ">=3.8" summary = "An implementation of JSON Schema validation for Python" groups = ["default"] @@ -1456,8 +1466,8 @@ dependencies = [ "rpds-py>=0.7.1", ] files = [ - {file = "jsonschema-4.22.0-py3-none-any.whl", hash = "sha256:ff4cfd6b1367a40e7bc6411caec72effadd3db0bbe5017de188f2d6108335802"}, - {file = "jsonschema-4.22.0.tar.gz", hash = "sha256:5b22d434a45935119af990552c862e5d6d564e8f6601206b305a61fdf661a2b7"}, + {file = "jsonschema-4.23.0-py3-none-any.whl", hash = "sha256:fbadb6f8b144a8f8cf9f0b89ba94501d143e50411a1278633f56a7acf7fd5566"}, + {file = "jsonschema-4.23.0.tar.gz", hash = "sha256:d71497fef26351a33265337fa77ffeb82423f3ea21283cd9467bb03999266bc4"}, ] [[package]] @@ -1720,13 +1730,13 @@ files = [ [[package]] name = "packaging" -version = "24.0" -requires_python = ">=3.7" +version = "24.1" +requires_python = ">=3.8" summary = "Core utilities for Python packages" groups = ["default", "dev"] files = [ - {file = "packaging-24.0-py3-none-any.whl", hash = "sha256:2ddfb553fdf02fb784c234c7ba6ccc288296ceabec964ad2eae3777778130bc5"}, - {file = "packaging-24.0.tar.gz", hash = "sha256:eb82c5e3e56209074766e6885bb04b8c38a0c015d0a30036ebe7ece34c9989e9"}, + {file = "packaging-24.1-py3-none-any.whl", hash = "sha256:5b8f2217dbdbd2f7f384c41c628544e6d52f2d0f53c6d0c3ea61aa5d1d7ff124"}, + {file = "packaging-24.1.tar.gz", hash = "sha256:026ed72c8ed3fcce5bf8950572258698927fd1dbda10a5e981cdf0ac37f4f002"}, ] [[package]] @@ -1806,13 +1816,13 @@ files = [ [[package]] name = "platformdirs" -version = "4.2.1" +version = "4.3.6" requires_python = ">=3.8" summary = "A small Python package for determining appropriate platform-specific dirs, e.g. a `user data dir`." groups = ["dev"] files = [ - {file = "platformdirs-4.2.1-py3-none-any.whl", hash = "sha256:17d5a1161b3fd67b390023cb2d3b026bbd40abde6fdb052dfbd3a29c3ba22ee1"}, - {file = "platformdirs-4.2.1.tar.gz", hash = "sha256:031cd18d4ec63ec53e82dceaac0417d218a6863f7745dfcc9efe7793b7039bdf"}, + {file = "platformdirs-4.3.6-py3-none-any.whl", hash = "sha256:73e575e1408ab8103900836b97580d5307456908a03e92031bab39e4554cc3fb"}, + {file = "platformdirs-4.3.6.tar.gz", hash = "sha256:357fb2acbc885b0419afd3ce3ed34564c13c9b95c89360cd9563f73aa5e2b907"}, ] [[package]] @@ -1896,13 +1906,13 @@ files = [ [[package]] name = "pycodestyle" -version = "2.11.1" +version = "2.12.1" requires_python = ">=3.8" summary = "Python style guide checker" groups = ["dev"] files = [ - {file = "pycodestyle-2.11.1-py2.py3-none-any.whl", hash = "sha256:44fe31000b2d866f2e41841b18528a505fbd7fef9017b04eff4e2648a0fadc67"}, - {file = "pycodestyle-2.11.1.tar.gz", hash = "sha256:41ba0e7afc9752dfb53ced5489e89f8186be00e599e712660695b7a75ff2663f"}, + {file = "pycodestyle-2.12.1-py2.py3-none-any.whl", hash = "sha256:46f0fb92069a7c28ab7bb558f05bfc0110dac69a0cd23c61ea0040283a9d78b3"}, + {file = "pycodestyle-2.12.1.tar.gz", hash = "sha256:6838eae08bbce4f6accd5d5572075c63626a15ee3e6f842df996bf62f6d73521"}, ] [[package]] @@ -2000,8 +2010,8 @@ requires_python = ">=3.7" summary = "JSON Web Token implementation in Python" groups = ["default"] dependencies = [ + "PyJWT==2.8.0", "cryptography>=3.4.0", - "pyjwt==2.8.0", ] files = [ {file = "PyJWT-2.8.0-py3-none-any.whl", hash = "sha256:59127c392cc44c2da5bb3192169a91f429924e17aff6534d70fdc02ab3e04320"}, @@ -2035,17 +2045,17 @@ files = [ [[package]] name = "pyproject-api" -version = "1.6.1" +version = "1.8.0" requires_python = ">=3.8" summary = "API to interact with the python pyproject.toml based projects" groups = ["dev"] dependencies = [ - "packaging>=23.1", + "packaging>=24.1", "tomli>=2.0.1; python_version < \"3.11\"", ] files = [ - {file = "pyproject_api-1.6.1-py3-none-any.whl", hash = "sha256:4c0116d60476b0786c88692cf4e325a9814965e2469c5998b830bba16b183675"}, - {file = "pyproject_api-1.6.1.tar.gz", hash = "sha256:1817dc018adc0d1ff9ca1ed8c60e1623d5aaca40814b953af14a9cf9a5cae538"}, + {file = "pyproject_api-1.8.0-py3-none-any.whl", hash = "sha256:3d7d347a047afe796fd5d1885b1e391ba29be7169bd2f102fcd378f04273d228"}, + {file = "pyproject_api-1.8.0.tar.gz", hash = "sha256:77b8049f2feb5d33eefcc21b57f1e279636277a8ac8ad6b5871037b243778496"}, ] [[package]] @@ -2278,7 +2288,7 @@ files = [ [[package]] name = "responses" -version = "0.25.0" +version = "0.25.3" requires_python = ">=3.8" summary = "A utility library for mocking out the `requests` Python library." groups = ["dev"] @@ -2288,8 +2298,8 @@ dependencies = [ "urllib3<3.0,>=1.25.10", ] files = [ - {file = "responses-0.25.0-py3-none-any.whl", hash = "sha256:2f0b9c2b6437db4b528619a77e5d565e4ec2a9532162ac1a131a83529db7be1a"}, - {file = "responses-0.25.0.tar.gz", hash = "sha256:01ae6a02b4f34e39bffceb0fc6786b67a25eae919c6368d05eabc8d9576c2a66"}, + {file = "responses-0.25.3-py3-none-any.whl", hash = "sha256:521efcbc82081ab8daa588e08f7e8a64ce79b91c39f6e62199b19159bea7dbcb"}, + {file = "responses-0.25.3.tar.gz", hash = "sha256:617b9247abd9ae28313d57a75880422d55ec63c29d33d629697590a034358dba"}, ] [[package]] @@ -2366,13 +2376,13 @@ files = [ [[package]] name = "setuptools" -version = "70.1.1" +version = "75.3.0" requires_python = ">=3.8" summary = "Easily download, build, install, upgrade, and uninstall Python packages" groups = ["default"] files = [ - {file = "setuptools-70.1.1-py3-none-any.whl", hash = "sha256:a58a8fde0541dab0419750bcc521fbdf8585f6e5cb41909df3a472ef7b81ca95"}, - {file = "setuptools-70.1.1.tar.gz", hash = "sha256:937a48c7cdb7a21eb53cd7f9b59e525503aa8abaf3584c730dc5f7a5bec3a650"}, + {file = "setuptools-75.3.0-py3-none-any.whl", hash = "sha256:f2504966861356aa38616760c0f66568e535562374995367b4e69c7143cf6bcd"}, + {file = "setuptools-75.3.0.tar.gz", hash = "sha256:fba5dd4d766e97be1b1681d98712680ae8f2f26d7881245f2ce9e40714f1a686"}, ] [[package]] @@ -2473,33 +2483,32 @@ files = [ [[package]] name = "sphinx" -version = "7.3.7" -requires_python = ">=3.9" +version = "8.1.3" +requires_python = ">=3.10" summary = "Python documentation generator" groups = ["dev"] dependencies = [ - "Jinja2>=3.0", - "Pygments>=2.14", - "alabaster~=0.7.14", - "babel>=2.9", - "colorama>=0.4.5; sys_platform == \"win32\"", - "docutils<0.22,>=0.18.1", + "Jinja2>=3.1", + "Pygments>=2.17", + "alabaster>=0.7.14", + "babel>=2.13", + "colorama>=0.4.6; sys_platform == \"win32\"", + "docutils<0.22,>=0.20", "imagesize>=1.3", - "importlib-metadata>=4.8; python_version < \"3.10\"", - "packaging>=21.0", - "requests>=2.25.0", - "snowballstemmer>=2.0", - "sphinxcontrib-applehelp", - "sphinxcontrib-devhelp", - "sphinxcontrib-htmlhelp>=2.0.0", - "sphinxcontrib-jsmath", - "sphinxcontrib-qthelp", + "packaging>=23.0", + "requests>=2.30.0", + "snowballstemmer>=2.2", + "sphinxcontrib-applehelp>=1.0.7", + "sphinxcontrib-devhelp>=1.0.6", + "sphinxcontrib-htmlhelp>=2.0.6", + "sphinxcontrib-jsmath>=1.0.1", + "sphinxcontrib-qthelp>=1.0.6", "sphinxcontrib-serializinghtml>=1.1.9", "tomli>=2; python_version < \"3.11\"", ] files = [ - {file = "sphinx-7.3.7-py3-none-any.whl", hash = "sha256:413f75440be4cacf328f580b4274ada4565fb2187d696a84970c23f77b64d8c3"}, - {file = "sphinx-7.3.7.tar.gz", hash = "sha256:a4a7db75ed37531c05002d56ed6948d4c42f473a36f46e1382b0bd76ca9627bc"}, + {file = "sphinx-8.1.3-py3-none-any.whl", hash = "sha256:09719015511837b76bf6e03e42eb7595ac8c2e41eeb9c29c5b755c6b677992a2"}, + {file = "sphinx-8.1.3.tar.gz", hash = "sha256:43c1911eecb0d3e161ad78611bc905d1ad0e523e4ddc202a58a821773dc4c927"}, ] [[package]] @@ -2526,13 +2535,13 @@ files = [ [[package]] name = "sphinxcontrib-htmlhelp" -version = "2.0.5" +version = "2.1.0" requires_python = ">=3.9" summary = "sphinxcontrib-htmlhelp is a sphinx extension which renders HTML help files" groups = ["dev"] files = [ - {file = "sphinxcontrib_htmlhelp-2.0.5-py3-none-any.whl", hash = "sha256:393f04f112b4d2f53d93448d4bce35842f62b307ccdc549ec1585e950bc35e04"}, - {file = "sphinxcontrib_htmlhelp-2.0.5.tar.gz", hash = "sha256:0dc87637d5de53dd5eec3a6a01753b1ccf99494bd756aafecd74b4fa9e729015"}, + {file = "sphinxcontrib_htmlhelp-2.1.0-py3-none-any.whl", hash = "sha256:166759820b47002d22914d64a075ce08f4c46818e17cfc9470a9786b759b19f8"}, + {file = "sphinxcontrib_htmlhelp-2.1.0.tar.gz", hash = "sha256:c9e2916ace8aad64cc13a0d233ee22317f2b9025b9cf3295249fa985cc7082e9"}, ] [[package]] @@ -2698,27 +2707,26 @@ files = [ [[package]] name = "tox" -version = "4.15.0" +version = "4.23.2" requires_python = ">=3.8" summary = "tox is a generic virtualenv management and test command line tool" groups = ["dev"] dependencies = [ - "cachetools>=5.3.2", + "cachetools>=5.5", "chardet>=5.2", "colorama>=0.4.6", - "filelock>=3.13.1", - "importlib-metadata>=7.0.1; python_version < \"3.8\"", - "packaging>=23.2", - "platformdirs>=4.1", - "pluggy>=1.3", - "pyproject-api>=1.6.1", + "filelock>=3.16.1", + "packaging>=24.1", + "platformdirs>=4.3.6", + "pluggy>=1.5", + "pyproject-api>=1.8", "tomli>=2.0.1; python_version < \"3.11\"", - "typing-extensions>=4.9; python_version < \"3.8\"", - "virtualenv>=20.25", + "typing-extensions>=4.12.2; python_version < \"3.11\"", + "virtualenv>=20.26.6", ] files = [ - {file = "tox-4.15.0-py3-none-any.whl", hash = "sha256:300055f335d855b2ab1b12c5802de7f62a36d4fd53f30bd2835f6a201dda46ea"}, - {file = "tox-4.15.0.tar.gz", hash = "sha256:7a0beeef166fbe566f54f795b4906c31b428eddafc0102ac00d20998dd1933f6"}, + {file = "tox-4.23.2-py3-none-any.whl", hash = "sha256:452bc32bb031f2282881a2118923176445bac783ab97c874b8770ab4c3b76c38"}, + {file = "tox-4.23.2.tar.gz", hash = "sha256:86075e00e555df6e82e74cfc333917f91ecb47ffbc868dcafbd2672e332f4a2c"}, ] [[package]] @@ -2771,7 +2779,10 @@ files = [ [[package]] name = "unicef-attachments" -version = "0.12" +version = "0.15" +git = "https://github.com/unicef/unicef-attachments.git" +ref = "b720a3ef26e3320a559f72ed1918c798dd2d7b67" +revision = "b720a3ef26e3320a559f72ed1918c798dd2d7b67" summary = "Django package that handles attachments" groups = ["default"] dependencies = [ @@ -2782,12 +2793,9 @@ dependencies = [ "drf-querystringfilter", "python-magic", "pytz", + "setuptools", "unicef-restlib", ] -files = [ - {file = "unicef_attachments-0.12-py2.py3-none-any.whl", hash = "sha256:4503eb6123162592ccc1588f16ea86f7096dc30b8fb0b08e14f894b72198c6e4"}, - {file = "unicef_attachments-0.12.tar.gz", hash = "sha256:0894eeed9353349d265ff2bf928e32c6e82bd07574a9f9d8dfc1a195077be991"}, -] [[package]] name = "unicef-djangolib" @@ -2947,19 +2955,21 @@ files = [ [[package]] name = "vcrpy" -version = "6.0.1" +version = "6.0.2" requires_python = ">=3.8" summary = "Automatically mock your HTTP interactions to simplify and speed up testing" groups = ["dev"] dependencies = [ "PyYAML", + "urllib3; platform_python_implementation != \"PyPy\" and python_version >= \"3.10\"", "urllib3<2; platform_python_implementation == \"PyPy\"", "urllib3<2; python_version < \"3.10\"", "wrapt", "yarl", ] files = [ - {file = "vcrpy-6.0.1.tar.gz", hash = "sha256:9e023fee7f892baa0bbda2f7da7c8ac51165c1c6e38ff8688683a12a4bde9278"}, + {file = "vcrpy-6.0.2-py2.py3-none-any.whl", hash = "sha256:40370223861181bc76a5e5d4b743a95058bb1ad516c3c08570316ab592f56cad"}, + {file = "vcrpy-6.0.2.tar.gz", hash = "sha256:88e13d9111846745898411dbc74a75ce85870af96dd320d75f1ee33158addc09"}, ] [[package]] @@ -2975,8 +2985,8 @@ files = [ [[package]] name = "virtualenv" -version = "20.26.2" -requires_python = ">=3.7" +version = "20.27.1" +requires_python = ">=3.8" summary = "Virtual Python Environment builder" groups = ["dev"] dependencies = [ @@ -2986,8 +2996,8 @@ dependencies = [ "platformdirs<5,>=3.9.1", ] files = [ - {file = "virtualenv-20.26.2-py3-none-any.whl", hash = "sha256:a624db5e94f01ad993d476b9ee5346fdf7b9de43ccaee0e0197012dc838a0e9b"}, - {file = "virtualenv-20.26.2.tar.gz", hash = "sha256:82bf0f4eebbb78d36ddaee0283d43fe5736b53880b8a8cdcd37390a07ac3741c"}, + {file = "virtualenv-20.27.1-py3-none-any.whl", hash = "sha256:f11f1b8a29525562925f745563bfd48b189450f61fb34c4f9cc79dd5aa32a1f4"}, + {file = "virtualenv-20.27.1.tar.gz", hash = "sha256:142c6be10212543b32c6c45d3d3893dff89112cc588b7d0879ae5a1ec03a47ba"}, ] [[package]] diff --git a/pyproject.toml b/pyproject.toml index ec6ab07be0..e6e8c0a31c 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -66,7 +66,7 @@ dependencies = [ 'social-auth-app-django==5.4.1', "social-auth-core[azuread]==4.5.4", 'tenant-schemas-celery==2.2.0', - 'unicef-attachments==0.12', + 'unicef-attachments @ git+https://github.com/unicef/unicef-attachments.git@b720a3ef26e3320a559f72ed1918c798dd2d7b67', 'unicef-djangolib==0.7', 'unicef-locations==4.2', "unicef-notification==1.4.1", diff --git a/src/etools/applications/attachments/permissions.py b/src/etools/applications/attachments/permissions.py index ddd3697b8d..a315c43df4 100644 --- a/src/etools/applications/attachments/permissions.py +++ b/src/etools/applications/attachments/permissions.py @@ -1,4 +1,6 @@ -from rest_framework.permissions import IsAuthenticated +from rest_framework.permissions import BasePermission, IsAuthenticated + +from etools.applications.core.permissions import IsUNICEFUser class IsInSchema(IsAuthenticated): @@ -6,3 +8,35 @@ def has_permission(self, request, view): super().has_permission(request, view) # make sure user has schema/tenant set return bool(hasattr(request, "tenant") and request.tenant) + + def has_object_permission(self, request, view, obj): + return self.has_permission(request, view) + + +class IsActiveInCurrentSchema(IsInSchema): + def has_permission(self, request, view): + return super().has_permission(request, view) and request.user.realms.filter( + country=request.tenant, + is_active=True, + ).exists() + + def has_object_permission(self, request, view, obj): + return self.has_permission(request, view) + + +class IsRelatedThirdPartyUser(BasePermission): + def has_permission(self, request, view): + return True + + def has_object_permission(self, request, view, obj): + content_object = obj.content_object + if not content_object: + return True + + if hasattr(content_object, 'get_related_third_party_users'): + return content_object.get_related_third_party_users().filter(pk=request.user.pk).exists() + + return False + + +UNICEFAttachmentsPermission = IsActiveInCurrentSchema & (IsUNICEFUser | IsRelatedThirdPartyUser) diff --git a/src/etools/applications/attachments/tests/test_permissions.py b/src/etools/applications/attachments/tests/test_permissions.py new file mode 100644 index 0000000000..cc640c6fc4 --- /dev/null +++ b/src/etools/applications/attachments/tests/test_permissions.py @@ -0,0 +1,561 @@ +from django.core.files.uploadedfile import SimpleUploadedFile +from django.db import connection +from django.urls import reverse + +from rest_framework import status + +from etools.applications.attachments.tests.factories import AttachmentFactory, AttachmentLinkFactory +from etools.applications.audit.models import Auditor +from etools.applications.audit.tests.factories import AuditorUserFactory, AuditPartnerFactory, SpecialAuditFactory +from etools.applications.core.tests.cases import BaseTenantTestCase +from etools.applications.field_monitoring.data_collection.tests.factories import ( + ChecklistOverallFindingFactory, + StartedChecklistFactory, +) +from etools.applications.field_monitoring.fm_settings.models import GlobalConfig +from etools.applications.field_monitoring.fm_settings.tests.factories import LogIssueFactory +from etools.applications.field_monitoring.groups import FMUser +from etools.applications.field_monitoring.planning.models import MonitoringActivity +from etools.applications.field_monitoring.planning.tests.factories import MonitoringActivityFactory +from etools.applications.partners.permissions import UNICEF_USER +from etools.applications.partners.tests.factories import PartnerFactory +from etools.applications.psea.models import Assessor +from etools.applications.psea.tests.factories import AnswerFactory, AssessmentFactory, AssessorFactory +from etools.applications.tpm.models import ThirdPartyMonitor +from etools.applications.tpm.tests.factories import ( + TPMActivityFactory, + TPMPartnerFactory, + TPMUserFactory, + TPMVisitFactory, +) +from etools.applications.users.tests.factories import DummyCountryFactory, GroupFactory, RealmFactory, UserFactory +from etools.libraries.djangolib.models import GroupWrapper + + +class DownloadAttachmentsBaseTestCase(BaseTenantTestCase): + def setUp(self): + super().setUp() + # clearing groups cache + GroupWrapper.invalidate_instances() + + self.unicef_user = UserFactory(is_staff=True) + self.attachment = AttachmentFactory( + file=SimpleUploadedFile( + 'simple_file.txt', + b'these are the file contents!' + ) + ) + + def _test_download(self, attachment, user, expected_status): + response = self.forced_auth_req('get', attachment.file_link, user=user) + self.assertEqual(response.status_code, expected_status) + return response + + +class DownloadUnlinkedAttachmentTestCase(DownloadAttachmentsBaseTestCase): + # anyone has access to attachment when it's not linked to any object + + def test_attachment_user_not_in_schema(self): + another_schema_user = UserFactory(is_staff=True, realms__data=[], profile__country=None) + self._test_download(self.attachment, another_schema_user, status.HTTP_403_FORBIDDEN) + + def test_attachment_user_in_different_schema(self): + other_country = DummyCountryFactory() + self.assertNotEqual(other_country.pk, connection.tenant.pk) + another_schema_user = UserFactory(is_staff=True) + another_schema_user.realms.update(is_active=False) + RealmFactory(user=another_schema_user, country=other_country, group=GroupFactory(name=UNICEF_USER)) + self._test_download(self.attachment, another_schema_user, status.HTTP_403_FORBIDDEN) + + def test_attachment_unicef(self): + self._test_download(self.attachment, self.unicef_user, status.HTTP_302_FOUND) + + def test_attachment_auditor(self): + auditor = AuditorUserFactory(is_staff=False) + self._test_download(self.attachment, auditor, status.HTTP_302_FOUND) + + +class DownloadAPAttachmentTestCase(DownloadAttachmentsBaseTestCase): + def setUp(self): + super().setUp() + self.auditor_firm = AuditPartnerFactory() + self.specialaudit = SpecialAuditFactory(agreement__auditor_firm=self.auditor_firm) + self.auditor = AuditorUserFactory(partner_firm=self.auditor_firm, is_staff=False) + self.attachment.content_object = self.specialaudit + self.attachment.save() + + def test_attachment_user_not_in_schema(self): + another_schema_user = UserFactory(is_staff=True, realms__data=[], profile__country=None) + self._test_download(self.attachment, another_schema_user, status.HTTP_403_FORBIDDEN) + + def test_attachment_unicef(self): + self._test_download(self.attachment, self.unicef_user, status.HTTP_302_FOUND) + + def test_attachment_authorized_officer(self): + self.specialaudit.staff_members.add(self.auditor) + self._test_download(self.attachment, self.auditor, status.HTTP_302_FOUND) + + def test_attachment_unrelated_auditor(self): + self._test_download(self.attachment, self.auditor, status.HTTP_403_FORBIDDEN) + + def test_attachment_deactivated_auditor(self): + auditor = AuditorUserFactory(partner_firm=self.auditor_firm, is_staff=False) + self.specialaudit.staff_members.add(auditor) + auditor.realms.update(is_active=False) + self._test_download(self.attachment, auditor, status.HTTP_403_FORBIDDEN) + + def test_attachment_moved_auditor(self): + # user should have no access if not active in the organization + # even if it's listed in the audit and has active realm with Auditor group + auditor_firm = AuditPartnerFactory() + auditor = AuditorUserFactory(partner_firm=auditor_firm, is_staff=False) + self.specialaudit.staff_members.add(auditor) + realm = RealmFactory( + user=auditor, + country=self.tenant, + organization=self.auditor_firm.organization, + group=Auditor.as_group() + ) + realm.is_active = False + realm.save() + self._test_download(self.attachment, auditor, status.HTTP_403_FORBIDDEN) + + +class DownloadTPMVisitAttachmentTestCase(DownloadAttachmentsBaseTestCase): + def setUp(self): + super().setUp() + self.tpm_organization = TPMPartnerFactory() + self.visit = TPMVisitFactory(tpm_partner=self.tpm_organization) + self.tpm_staff = TPMUserFactory(tpm_partner=self.tpm_organization, is_staff=False) + self.attachment.content_object = self.visit + self.attachment.save() + + def test_attachment_user_not_in_schema(self): + another_schema_user = UserFactory(is_staff=True, realms__data=[], profile__country=None) + self._test_download(self.attachment, another_schema_user, status.HTTP_403_FORBIDDEN) + + def test_attachment_unicef(self): + self._test_download(self.attachment, self.unicef_user, status.HTTP_302_FOUND) + + def test_attachment_staff_member(self): + self._test_download(self.attachment, self.tpm_staff, status.HTTP_302_FOUND) + + def test_attachment_unrelated_staff(self): + another_tpm_staff = TPMUserFactory(is_staff=False) + self._test_download(self.attachment, another_tpm_staff, status.HTTP_403_FORBIDDEN) + + def test_attachment_deactivated_staff(self): + staff = TPMUserFactory(tpm_partner=self.tpm_organization, is_staff=False) + self.visit.tpm_partner_focal_points.add(staff) + staff.realms.update(is_active=False) + self._test_download(self.attachment, staff, status.HTTP_403_FORBIDDEN) + + def test_attachment_moved_staff(self): + # user should have no access if not active in the organization + # even if it's listed in the visit and has active realm with TPM group + tpm_organization = TPMPartnerFactory() + staff = TPMUserFactory(tpm_partner=tpm_organization, is_staff=False) + self.visit.tpm_partner_focal_points.add(staff) + realm = RealmFactory( + user=staff, + country=self.tenant, + organization=self.tpm_organization.organization, + group=ThirdPartyMonitor.as_group() + ) + realm.is_active = False + realm.save() + self._test_download(self.attachment, staff, status.HTTP_403_FORBIDDEN) + + +class DownloadTPMVisitActivityAttachmentTestCase(DownloadAttachmentsBaseTestCase): + def setUp(self): + super().setUp() + self.tpm_organization = TPMPartnerFactory() + self.visit = TPMVisitFactory(tpm_partner=self.tpm_organization) + self.tpm_staff = TPMUserFactory(tpm_partner=self.tpm_organization, is_staff=False) + self.activity = TPMActivityFactory(tpm_visit=self.visit) + self.attachment.content_object = self.activity + self.attachment.save() + + def test_attachment_user_not_in_schema(self): + another_schema_user = UserFactory(is_staff=True, realms__data=[], profile__country=None) + self._test_download(self.attachment, another_schema_user, status.HTTP_403_FORBIDDEN) + + def test_attachment_unicef(self): + self._test_download(self.attachment, self.unicef_user, status.HTTP_302_FOUND) + + def test_attachment_staff_member(self): + self._test_download(self.attachment, self.tpm_staff, status.HTTP_302_FOUND) + + def test_attachment_unrelated_staff(self): + another_tpm_staff = TPMUserFactory(is_staff=False) + self._test_download(self.attachment, another_tpm_staff, status.HTTP_403_FORBIDDEN) + + def test_attachment_deactivated_staff(self): + staff = TPMUserFactory(tpm_partner=self.tpm_organization, is_staff=False) + self.visit.tpm_partner_focal_points.add(staff) + staff.realms.update(is_active=False) + self._test_download(self.attachment, staff, status.HTTP_403_FORBIDDEN) + + def test_attachment_moved_staff(self): + # user should have no access if not active in the organization + # even if it's listed in the visit and has active realm with TPM group + tpm_organization = TPMPartnerFactory() + staff = TPMUserFactory(tpm_partner=tpm_organization, is_staff=False) + self.visit.tpm_partner_focal_points.add(staff) + realm = RealmFactory( + user=staff, + country=self.tenant, + organization=self.tpm_organization.organization, + group=ThirdPartyMonitor.as_group() + ) + realm.is_active = False + realm.save() + self._test_download(self.attachment, staff, status.HTTP_403_FORBIDDEN) + + +class DownloadTPMPartnerAttachmentTestCase(DownloadAttachmentsBaseTestCase): + def setUp(self): + super().setUp() + self.tpm_organization = TPMPartnerFactory() + self.tpm_staff = TPMUserFactory(tpm_partner=self.tpm_organization, is_staff=False) + self.attachment.content_object = self.tpm_organization + self.attachment.save() + + def test_attachment_user_not_in_schema(self): + another_schema_user = UserFactory(is_staff=True, realms__data=[], profile__country=None) + self._test_download(self.attachment, another_schema_user, status.HTTP_403_FORBIDDEN) + + def test_attachment_unicef(self): + self._test_download(self.attachment, self.unicef_user, status.HTTP_302_FOUND) + + def test_attachment_staff_member(self): + self._test_download(self.attachment, self.tpm_staff, status.HTTP_302_FOUND) + + def test_attachment_unrelated_staff(self): + another_tpm_staff = TPMUserFactory(is_staff=False) + self._test_download(self.attachment, another_tpm_staff, status.HTTP_403_FORBIDDEN) + + def test_attachment_deactivated_staff(self): + staff = TPMUserFactory(tpm_partner=self.tpm_organization, is_staff=False) + staff.realms.update(is_active=False) + self._test_download(self.attachment, staff, status.HTTP_403_FORBIDDEN) + + def test_attachment_moved_staff(self): + # user should have no access if not active in the organization + # even if it's listed in the visit and has active realm with TPM group + tpm_organization = TPMPartnerFactory() + staff = TPMUserFactory(tpm_partner=tpm_organization, is_staff=False) + realm = RealmFactory( + user=staff, + country=self.tenant, + organization=self.tpm_organization.organization, + group=ThirdPartyMonitor.as_group() + ) + realm.is_active = False + realm.save() + self._test_download(self.attachment, staff, status.HTTP_403_FORBIDDEN) + + +class DownloadFMGlobalConfigAttachmentTestCase(DownloadAttachmentsBaseTestCase): + @classmethod + def setUpClass(cls): + super().setUpClass() + cls.config = GlobalConfig.get_current() + + def setUp(self): + super().setUp() + # FM user is always UNICEF user + self.fm_user = UserFactory(is_staff=False, realms__data=[UNICEF_USER, FMUser.name]) + self.attachment.content_object = self.config + self.attachment.save() + + def test_attachment_user_not_in_schema(self): + another_schema_user = UserFactory(is_staff=True, realms__data=[], profile__country=None) + self._test_download(self.attachment, another_schema_user, status.HTTP_403_FORBIDDEN) + + def test_attachment_unicef(self): + self._test_download(self.attachment, self.unicef_user, status.HTTP_302_FOUND) + + def test_attachment_fm_user(self): + self._test_download(self.attachment, self.fm_user, status.HTTP_302_FOUND) + + def test_attachment_not_fm_user(self): + user = UserFactory(is_staff=False, realms__data=["Unknown"]) + self._test_download(self.attachment, user, status.HTTP_403_FORBIDDEN) + + +class DownloadFMLogIssueAttachmentTestCase(DownloadAttachmentsBaseTestCase): + def setUp(self): + super().setUp() + self.tpm_organization = TPMPartnerFactory() + # FM user is always UNICEF user + self.fm_user = UserFactory(is_staff=False, realms__data=[UNICEF_USER, FMUser.name]) + self.log_issue = LogIssueFactory(partner=PartnerFactory()) + self.attachment.content_object = self.log_issue + self.attachment.save() + + def test_attachment_user_not_in_schema(self): + another_schema_user = UserFactory(is_staff=True, realms__data=[], profile__country=None) + self._test_download(self.attachment, another_schema_user, status.HTTP_403_FORBIDDEN) + + def test_attachment_unicef(self): + self._test_download(self.attachment, self.unicef_user, status.HTTP_302_FOUND) + + def test_attachment_fm_user(self): + self._test_download(self.attachment, self.fm_user, status.HTTP_302_FOUND) + + def test_attachment_not_fm_user(self): + user = UserFactory(is_staff=False, realms__data=["Unknown"]) + self._test_download(self.attachment, user, status.HTTP_403_FORBIDDEN) + + +class DownloadFMActivityAttachmentTestCase(DownloadAttachmentsBaseTestCase): + def setUp(self): + super().setUp() + self.tpm_organization = TPMPartnerFactory() + self.tpm_staff = TPMUserFactory(tpm_partner=self.tpm_organization, is_staff=False) + self.activity = MonitoringActivityFactory( + tpm_partner=self.tpm_organization, + monitor_type=MonitoringActivity.MONITOR_TYPE_CHOICES.tpm, + ) + self.attachment.content_object = self.activity + self.attachment.save() + + def test_attachment_user_not_in_schema(self): + another_schema_user = UserFactory(is_staff=True, realms__data=[], profile__country=None) + self._test_download(self.attachment, another_schema_user, status.HTTP_403_FORBIDDEN) + + def test_attachment_unicef(self): + self._test_download(self.attachment, self.unicef_user, status.HTTP_302_FOUND) + + def test_attachment_visit_lead(self): + self.activity.visit_lead = self.tpm_staff + self.activity.save() + self._test_download(self.attachment, self.tpm_staff, status.HTTP_302_FOUND) + + def test_attachment_team_member(self): + self.activity.team_members.add(self.tpm_staff) + self._test_download(self.attachment, self.tpm_staff, status.HTTP_302_FOUND) + + def test_attachment_unrelated_staff(self): + self._test_download(self.attachment, self.tpm_staff, status.HTTP_403_FORBIDDEN) + + +class DownloadFMActivityCheckListAttachmentTestCase(DownloadAttachmentsBaseTestCase): + def setUp(self): + super().setUp() + self.tpm_organization = TPMPartnerFactory() + self.tpm_staff = TPMUserFactory(tpm_partner=self.tpm_organization, is_staff=False) + self.activity = MonitoringActivityFactory( + tpm_partner=self.tpm_organization, + monitor_type=MonitoringActivity.MONITOR_TYPE_CHOICES.tpm, + ) + self.started_checklist = StartedChecklistFactory(monitoring_activity=self.activity) + self.checklist_overall_finding = ChecklistOverallFindingFactory( + started_checklist=self.started_checklist, + partner=PartnerFactory(), + ) + self.attachment.content_object = self.checklist_overall_finding + self.attachment.save() + + def test_attachment_user_not_in_schema(self): + another_schema_user = UserFactory(is_staff=True, realms__data=[], profile__country=None) + self._test_download(self.attachment, another_schema_user, status.HTTP_403_FORBIDDEN) + + def test_attachment_unicef(self): + self._test_download(self.attachment, self.unicef_user, status.HTTP_302_FOUND) + + def test_attachment_visit_lead(self): + self.activity.visit_lead = self.tpm_staff + self.activity.save() + self._test_download(self.attachment, self.tpm_staff, status.HTTP_302_FOUND) + + def test_attachment_team_member(self): + self.activity.team_members.add(self.tpm_staff) + self._test_download(self.attachment, self.tpm_staff, status.HTTP_302_FOUND) + + def test_attachment_unrelated_staff(self): + self._test_download(self.attachment, self.tpm_staff, status.HTTP_403_FORBIDDEN) + + +class DownloadPSEAAssessmentAttachmentTestCase(DownloadAttachmentsBaseTestCase): + def setUp(self): + super().setUp() + self.auditor_firm = AuditPartnerFactory() + self.auditor = AuditorUserFactory(partner_firm=self.auditor_firm, is_staff=False) + self.assessment = AssessmentFactory() + self.attachment.content_object = self.assessment + self.attachment.save() + + def test_attachment_user_not_in_schema(self): + another_schema_user = UserFactory(is_staff=True, realms__data=[], profile__country=None) + self._test_download(self.attachment, another_schema_user, status.HTTP_403_FORBIDDEN) + + def test_attachment_unicef(self): + self._test_download(self.attachment, self.unicef_user, status.HTTP_302_FOUND) + + def test_attachment_external_accessor(self): + external_user = UserFactory( + is_staff=False, realms__data=[Auditor.name] + ) + AssessorFactory( + assessment=self.assessment, + assessor_type=Assessor.TYPE_EXTERNAL, + user=external_user, + ) + self._test_download(self.attachment, external_user, status.HTTP_302_FOUND) + + def test_attachment_unrelated_staff(self): + AssessorFactory( + assessment=self.assessment, + assessor_type=Assessor.TYPE_VENDOR, + auditor_firm=self.auditor_firm, + user=None, + ) + self._test_download(self.attachment, self.auditor, status.HTTP_403_FORBIDDEN) + + def test_attachment_staff(self): + assessor = AssessorFactory( + assessment=self.assessment, + assessor_type=Assessor.TYPE_VENDOR, + auditor_firm=self.auditor_firm, + user=None, + ) + assessor.auditor_firm_staff.add(self.auditor) + self._test_download(self.attachment, self.auditor, status.HTTP_302_FOUND) + + +class DownloadPSEAAnswerAttachmentTestCase(DownloadAttachmentsBaseTestCase): + def setUp(self): + super().setUp() + self.auditor_firm = AuditPartnerFactory() + self.auditor = AuditorUserFactory(partner_firm=self.auditor_firm, is_staff=False) + self.assessment = AssessmentFactory() + self.answer = AnswerFactory(assessment=self.assessment) + self.attachment.content_object = self.answer + self.attachment.save() + + def test_attachment_user_not_in_schema(self): + another_schema_user = UserFactory(is_staff=True, realms__data=[], profile__country=None) + self._test_download(self.attachment, another_schema_user, status.HTTP_403_FORBIDDEN) + + def test_attachment_unicef(self): + AssessorFactory( + assessment=self.assessment, + assessor_type=Assessor.TYPE_UNICEF, + ) + self._test_download(self.attachment, self.unicef_user, status.HTTP_302_FOUND) + + def test_attachment_unicef_auditor(self): + AssessorFactory( + assessment=self.assessment, + assessor_type=Assessor.TYPE_UNICEF, + ) + self._test_download(self.attachment, self.auditor, status.HTTP_403_FORBIDDEN) + + def test_attachment_external_accessor(self): + external_user = UserFactory(is_staff=False, realms__data=[Auditor.name]) + AssessorFactory( + assessment=self.assessment, + assessor_type=Assessor.TYPE_EXTERNAL, + user=external_user, + ) + self._test_download(self.attachment, external_user, status.HTTP_302_FOUND) + + def test_attachment_external_not_accessor(self): + external_user = UserFactory(is_staff=False, realms__data=[Auditor.name]) + AssessorFactory( + assessment=self.assessment, + assessor_type=Assessor.TYPE_EXTERNAL, + ) + self._test_download(self.attachment, external_user, status.HTTP_403_FORBIDDEN) + + def test_attachment_auditor_unrelated_staff(self): + AssessorFactory( + assessment=self.assessment, + assessor_type=Assessor.TYPE_VENDOR, + auditor_firm=self.auditor_firm, + user=None, + ) + self._test_download(self.attachment, self.auditor, status.HTTP_403_FORBIDDEN) + + def test_attachment_auditor_related_staff(self): + assessor = AssessorFactory( + assessment=self.assessment, + assessor_type=Assessor.TYPE_VENDOR, + auditor_firm=self.auditor_firm, + user=None, + ) + assessor.auditor_firm_staff.add(self.auditor) + self._test_download(self.attachment, self.auditor, status.HTTP_302_FOUND) + + +class AttachmentLinkBaseTestCase(BaseTenantTestCase): + def setUp(self): + super().setUp() + # clearing groups cache + GroupWrapper.invalidate_instances() + + self.unicef_user = UserFactory(is_staff=True) + self.attachment_link = AttachmentLinkFactory( + attachment__file=SimpleUploadedFile( + 'simple_file.txt', + b'these are the file contents!' + ) + ) + + def _test_delete(self, attachment_link, user, expected_status): + response = self.forced_auth_req( + 'delete', + reverse('attachments:link-delete', args=[attachment_link.pk]), + user=user, + ) + self.assertEqual(response.status_code, expected_status) + return response + + +class TPMVisitAttachmentLinkTestCase(AttachmentLinkBaseTestCase): + def setUp(self): + super().setUp() + self.tpm_organization = TPMPartnerFactory() + self.tpm_staff = TPMUserFactory(tpm_partner=self.tpm_organization, is_staff=False) + self.visit = TPMVisitFactory(tpm_partner=self.tpm_organization) + self.attachment_link.content_object = self.visit + self.attachment_link.save() + + def test_attachment_user_not_in_schema(self): + another_schema_user = UserFactory(is_staff=True, realms__data=[], profile__country=None) + self._test_delete(self.attachment_link, another_schema_user, status.HTTP_403_FORBIDDEN) + + def test_attachment_unicef(self): + self._test_delete(self.attachment_link, self.unicef_user, status.HTTP_204_NO_CONTENT) + + def test_attachment_staff_member(self): + self._test_delete(self.attachment_link, self.tpm_staff, status.HTTP_204_NO_CONTENT) + + def test_attachment_unrelated_staff(self): + another_tpm_staff = TPMUserFactory(is_staff=False) + self._test_delete(self.attachment_link, another_tpm_staff, status.HTTP_403_FORBIDDEN) + + def test_attachment_deactivated_staff(self): + staff = TPMUserFactory(tpm_partner=self.tpm_organization, is_staff=False) + self.visit.tpm_partner_focal_points.add(staff) + staff.realms.update(is_active=False) + self._test_delete(self.attachment_link, staff, status.HTTP_403_FORBIDDEN) + + def test_attachment_moved_staff(self): + # user should have no access if not active in the organization + # even if it's listed in the visit and has active realm with TPM group + tpm_organization = TPMPartnerFactory() + staff = TPMUserFactory(tpm_partner=tpm_organization, is_staff=False) + self.visit.tpm_partner_focal_points.add(staff) + realm = RealmFactory( + user=staff, + country=self.tenant, + organization=self.tpm_organization.organization, + group=ThirdPartyMonitor.as_group() + ) + realm.is_active = False + realm.save() + self._test_delete(self.attachment_link, staff, status.HTTP_403_FORBIDDEN) diff --git a/src/etools/applications/audit/models.py b/src/etools/applications/audit/models.py index 329d4cd2f5..5934d1d724 100644 --- a/src/etools/applications/audit/models.py +++ b/src/etools/applications/audit/models.py @@ -314,6 +314,15 @@ def save(self, *args, **kwargs): self.reference_number = self.get_reference_number() self.save() + def get_related_third_party_users(self): + return get_user_model().objects.filter( + models.Q(pk__in=self.authorized_officers.values_list('id')) | + models.Q(pk__in=self.staff_members.values_list('id')) + ).filter( + realms__organization=self.agreement.auditor_firm.organization, + realms__is_active=True, + ) + class RiskCategory(OrderedModel, models.Model): """Group of questions""" diff --git a/src/etools/applications/audit/serializers/engagement.py b/src/etools/applications/audit/serializers/engagement.py index 3950ed01a7..ec50b13591 100644 --- a/src/etools/applications/audit/serializers/engagement.py +++ b/src/etools/applications/audit/serializers/engagement.py @@ -75,10 +75,7 @@ def to_representation(self, value): return None attachment = Attachment.objects.get(pk=value) - if not getattr(attachment.file, "url", None): - return None - - url = attachment.file.url + url = attachment.file_link request = self.context.get('request', None) if request is not None: return request.build_absolute_uri(url) diff --git a/src/etools/applications/core/permissions.py b/src/etools/applications/core/permissions.py index 59438a9acc..ab86107d98 100644 --- a/src/etools/applications/core/permissions.py +++ b/src/etools/applications/core/permissions.py @@ -100,3 +100,6 @@ class IsUNICEFUser(IsAuthenticated): def has_permission(self, request, view): return super().has_permission(request, view) and request.user.groups.filter(name='UNICEF User').exists() + + def has_object_permission(self, request, view, obj): + return self.has_permission(request, view) diff --git a/src/etools/applications/field_monitoring/data_collection/models.py b/src/etools/applications/field_monitoring/data_collection/models.py index 61396d9e11..907b420cdd 100644 --- a/src/etools/applications/field_monitoring/data_collection/models.py +++ b/src/etools/applications/field_monitoring/data_collection/models.py @@ -168,6 +168,9 @@ class Meta: def __str__(self): return '{} - {}'.format(self.started_checklist, self.narrative_finding) + def get_related_third_party_users(self): + return self.started_checklist.monitoring_activity.get_related_third_party_users() + class ActivityOverallFindingQuerySet(models.QuerySet): def annotate_for_activity_export(self): diff --git a/src/etools/applications/field_monitoring/planning/models.py b/src/etools/applications/field_monitoring/planning/models.py index 1d9fe640e0..8a5046a105 100644 --- a/src/etools/applications/field_monitoring/planning/models.py +++ b/src/etools/applications/field_monitoring/planning/models.py @@ -729,6 +729,19 @@ def get_export_checklist_findings(self): yield checklist_dict + def get_related_third_party_users(self): + if not self.tpm_partner: + return get_user_model().objects.none() + + return get_user_model().objects.filter( + Q(pk=self.visit_lead_id) | + Q(pk__in=self.team_members.through.objects + .filter(monitoringactivity_id=self.pk).values_list('user_id', flat=True)) + ).filter( + realms__organization=self.tpm_partner.organization, + realms__is_active=True, + ) + class MonitoringActivityActionPointManager(models.Manager): def get_queryset(self): diff --git a/src/etools/applications/last_mile/admin.py b/src/etools/applications/last_mile/admin.py index e54cc35773..821c8425f6 100644 --- a/src/etools/applications/last_mile/admin.py +++ b/src/etools/applications/last_mile/admin.py @@ -84,6 +84,8 @@ def import_data(self, workbook): p_code = poi_dict.get('p_code', None) if not p_code or p_code == "None": # add a pcode if it doesn't exist: + p_code = poi_dict.get('p_code', None) + if not p_code or p_code == "None": poi_dict['p_code'] = generate_hash(poi_dict['partner_org_vendor_no'] + poi_dict['name'] + poi_dict['poi_type'], 12) long = poi_dict.pop('longitude') lat = poi_dict.pop('latitude') @@ -113,9 +115,9 @@ def import_data(self, workbook): poi_obj, _ = models.PointOfInterest.all_objects.update_or_create( p_code=poi_dict['p_code'], defaults={'private': poi_dict['private'], - 'point': poi_dict['point'], - 'name': poi_dict['name'], - 'poi_type': poi_dict.get('poi_type')} + "point": poi_dict['point'], + "name": poi_dict['name'], + "poi_type": poi_dict.get('poi_type')} ) poi_obj.partner_organizations.add(partner_org_obj) diff --git a/src/etools/applications/last_mile/tests/test_views.py b/src/etools/applications/last_mile/tests/test_views.py index c14983720b..2819296ac0 100644 --- a/src/etools/applications/last_mile/tests/test_views.py +++ b/src/etools/applications/last_mile/tests/test_views.py @@ -293,7 +293,7 @@ def test_partial_checkin_with_short(self): self.assertEqual(response.status_code, status.HTTP_200_OK) self.incoming.refresh_from_db() self.assertEqual(self.incoming.status, models.Transfer.COMPLETED) - self.assertIn(response.data['proof_file'], self.attachment.file.path) + self.assertIn(response.data['proof_file'], self.attachment.file_link) self.assertIn(f'DW @ {checkin_data["destination_check_in_at"].strftime("%y-%m-%d")}', self.incoming.name) self.assertEqual(self.incoming.items.count(), len(response.data['items'])) @@ -334,7 +334,7 @@ def test_partial_checkin_with_short_surplus(self): self.assertEqual(response.status_code, status.HTTP_200_OK) self.incoming.refresh_from_db() self.assertEqual(self.incoming.status, models.Transfer.COMPLETED) - self.assertIn(response.data['proof_file'], self.attachment.file.path) + self.assertIn(response.data['proof_file'], self.attachment.file_link) self.assertEqual(self.incoming.name, checkin_data['name']) self.assertEqual(self.incoming.items.count(), len(response.data['items'])) self.assertEqual(self.incoming.items.get(pk=item_1.pk).quantity, 11) @@ -382,7 +382,7 @@ def test_partial_checkin_RUFT_material(self): self.assertEqual(response.status_code, status.HTTP_200_OK) self.incoming.refresh_from_db() self.assertEqual(self.incoming.status, models.Transfer.COMPLETED) - self.assertIn(response.data['proof_file'], self.attachment.file.path) + self.assertIn(response.data['proof_file'], self.attachment.file_link) self.assertEqual(self.incoming.name, checkin_data['name']) self.assertEqual(self.incoming.items.count(), len(response.data['items'])) self.assertEqual(self.incoming.items.count(), 1) # only 1 checked-in item is visible, non RUFT @@ -482,7 +482,7 @@ def test_checkout_distribution(self): self.assertEqual(response.status_code, status.HTTP_200_OK) self.assertEqual(response.data['status'], models.Transfer.PENDING) self.assertEqual(response.data['transfer_type'], models.Transfer.DISTRIBUTION) - self.assertIn(response.data['proof_file'], self.attachment.file.path) + self.assertIn(response.data['proof_file'], self.attachment.file_link) checkout_transfer = models.Transfer.objects.get(pk=response.data['id']) self.assertEqual(checkout_transfer.destination_point, self.hospital) @@ -519,7 +519,7 @@ def test_checkout_wastage(self): self.assertEqual(response.status_code, status.HTTP_200_OK) self.assertEqual(response.data['status'], models.Transfer.COMPLETED) self.assertEqual(response.data['transfer_type'], models.Transfer.WASTAGE) - self.assertIn(response.data['proof_file'], self.attachment.file.path) + self.assertIn(response.data['proof_file'], self.attachment.file_link) wastage_transfer = models.Transfer.objects.get(pk=response.data['id']) self.assertEqual(wastage_transfer.destination_point, destination) @@ -554,7 +554,7 @@ def test_checkout_handover(self): self.assertEqual(response.status_code, status.HTTP_200_OK) self.assertEqual(response.data['status'], models.Transfer.PENDING) self.assertEqual(response.data['transfer_type'], models.Transfer.HANDOVER) - self.assertIn(response.data['proof_file'], self.attachment.file.path) + self.assertIn(response.data['proof_file'], self.attachment.file_link) handover_transfer = models.Transfer.objects.get(pk=response.data['id']) self.assertEqual(handover_transfer.partner_organization, agreement.partner) @@ -613,7 +613,7 @@ def test_checkout_wastage_without_location(self): self.assertEqual(response.status_code, status.HTTP_200_OK) self.assertEqual(response.data['status'], models.Transfer.COMPLETED) self.assertEqual(response.data['transfer_type'], models.Transfer.WASTAGE) - self.assertIn(response.data['proof_file'], self.attachment.file.path) + self.assertIn(response.data['proof_file'], self.attachment.file_link) wastage_transfer = models.Transfer.objects.get(pk=response.data['id']) self.assertEqual(wastage_transfer.destination_point, None) diff --git a/src/etools/applications/last_mile/views.py b/src/etools/applications/last_mile/views.py index 5546b6f142..8c152bef82 100644 --- a/src/etools/applications/last_mile/views.py +++ b/src/etools/applications/last_mile/views.py @@ -7,6 +7,7 @@ from django.utils.functional import cached_property from django.utils.translation import gettext as _ +import requests from django_filters.rest_framework import DjangoFilterBackend from rest_framework import mixins, status from rest_framework.decorators import action @@ -411,18 +412,36 @@ def get_pbi_access_token(): def pbi_headers(self): return {'Content-Type': 'application/json', 'Authorization': 'Bearer ' + self.get_pbi_access_token()} + def get_embed_url(self, workspace_id, report_id): + url_to_call = f'https://api.powerbi.com/v1.0/myorg/groups/{workspace_id}/reports/{report_id}' + api_response = requests.get(url_to_call, headers=self.pbi_headers) + if api_response.status_code == 200: + r = api_response.json() + return r["embedUrl"], r["datasetId"] + + def get_embed_token(self, dataset_id, workspace_id, report_id): + embed_token_api = 'https://api.powerbi.com/v1.0/myorg/GenerateToken' + request_body = { + "datasets": [{'id': dataset_id}], + "reports": [{'id': report_id}], + "targetWorkspaces": [{'id': workspace_id}] + } + api_response = requests.post(embed_token_api, data=request_body, headers=self.pbi_headers) + if api_response.status_code == 200: + return api_response.json()["token"] + return None + def get(self, request, *args, **kwargs): try: embed_url, dataset_id = get_embed_url(self.pbi_headers) + print(embed_url, 'embedurl') embed_token = get_embed_token(dataset_id, self.pbi_headers) + print(embed_token) except TokenRetrieveException: - return Response("Temporary unavailable, PowerBI information cannot be retrieved from Microsoft Servers", - status=status.HTTP_503_SERVICE_UNAVAILABLE) - + raise PermissionDenied('Token cannot be retrieved') resp_data = { "report_id": settings.PBI_CONFIG["REPORT_ID"], "embed_url": embed_url, - "access_token": embed_token, - "vendor_number": request.user.profile.organization.vendor_number + "access_token": embed_token } return Response(resp_data, status=status.HTTP_200_OK) diff --git a/src/etools/applications/partners/models.py b/src/etools/applications/partners/models.py index 5ae418df97..2603f8b124 100644 --- a/src/etools/applications/partners/models.py +++ b/src/etools/applications/partners/models.py @@ -2,6 +2,7 @@ import decimal from django.conf import settings +from django.contrib.auth import get_user_model from django.contrib.contenttypes.models import ContentType from django.contrib.postgres.fields import ArrayField from django.core.validators import MinValueValidator @@ -2571,6 +2572,15 @@ def was_active_before(self): change__status__after__in=[self.SIGNED, self.ACTIVE], ).exists() + def get_related_third_party_users(self): + qs_filter = Q(pk__in=self.partner_focal_points.values_list('user_id')) + if self.partner_authorized_officer_signatory: + qs_filter = qs_filter | Q(pk=self.partner_authorized_officer_signatory.user_id) + return get_user_model().objects.filter(qs_filter).filter( + realms__organization=self.agreement.partner, + realms__is_active=True, + ) + class InterventionAmendment(TimeStampedModel): """ diff --git a/src/etools/applications/partners/tests/test_api_agreements.py b/src/etools/applications/partners/tests/test_api_agreements.py index 4d58acc58f..f479f95b7d 100644 --- a/src/etools/applications/partners/tests/test_api_agreements.py +++ b/src/etools/applications/partners/tests/test_api_agreements.py @@ -97,7 +97,7 @@ def test_agreement_detail_attachment(self): status_code, response = self.run_request(self.agreement1.pk) self.assertEqual(status_code, status.HTTP_200_OK) - self.assertTrue(response["attachment"].endswith(attachment.file.url)) + self.assertTrue(response["attachment"].endswith(attachment.file_link)) def test_add_new_PCA(self): self.assertFalse(Activity.objects.exists()) @@ -399,7 +399,7 @@ def test_patch_agreement_with_attachment_as_pk(self): ) self.assertEqual(status_code, status.HTTP_200_OK) - self.assertTrue(response["attachment"].endswith(attachment.file.url)) + self.assertTrue(response["attachment"].endswith(attachment.file_link)) attachment_update = Attachment.objects.get(pk=attachment.pk) self.assertEqual(attachment_update.content_object, agreement) self.assertEqual(attachment_update.file_type, self.file_type_agreement) @@ -450,7 +450,7 @@ def test_patch_agreement_replace_attachment(self): status_code, response = self.run_request(agreement.pk) self.assertEqual(status_code, status.HTTP_200_OK) self.assertTrue( - response["attachment"].endswith(attachment_current.file.url) + response["attachment"].endswith(attachment_current.file_link) ) data = { @@ -464,7 +464,7 @@ def test_patch_agreement_replace_attachment(self): self.assertEqual(status_code, status.HTTP_200_OK) self.assertTrue( - response["attachment"].endswith(attachment_new.file.url) + response["attachment"].endswith(attachment_new.file_link) ) agreement_updated = Agreement.objects.get(pk=agreement.pk) self.assertEqual(agreement_updated.attachment.last(), attachment_new) diff --git a/src/etools/applications/partners/tests/test_api_partners.py b/src/etools/applications/partners/tests/test_api_partners.py index e7fd709925..9026f4fca9 100644 --- a/src/etools/applications/partners/tests/test_api_partners.py +++ b/src/etools/applications/partners/tests/test_api_partners.py @@ -198,7 +198,7 @@ def test_patch_with_core_values_assessment_attachment(self): self.assertEqual(len(data["core_values_assessments"]), 1) self.assertEqual( data["core_values_assessments"][0]["attachment"], - attachment.file.url + attachment.file_link ) def test_patch_with_assessment_attachment(self): @@ -234,7 +234,7 @@ def test_patch_with_assessment_attachment(self): self.assertEqual(len(data["assessments"]), 1) self.assertEqual( data["assessments"][0]["report_attachment"], - attachment.file.url + attachment.file_link ) def test_add_planned_visits(self): diff --git a/src/etools/applications/psea/models.py b/src/etools/applications/psea/models.py index be7a899058..a1ea5cb7a6 100644 --- a/src/etools/applications/psea/models.py +++ b/src/etools/applications/psea/models.py @@ -1,4 +1,5 @@ from django.conf import settings +from django.contrib.auth import get_user_model from django.db import connection, models from django.db.models import Sum from django.urls import reverse @@ -425,6 +426,18 @@ def transition_to_rejected_invalid(self): def transition_to_cancelled_invalid(self): """Allowed to move to cancelled status, except from submitted/final""" + def get_related_third_party_users(self): + if self.assessor.assessor_type == Assessor.TYPE_EXTERNAL: + return get_user_model().objects.filter(pk=self.assessor.user_id) + elif self.assessor.assessor_type == Assessor.TYPE_VENDOR: + if self.assessor.auditor_firm: + return self.assessor.auditor_firm_staff.filter( + realms__organization=self.assessor.auditor_firm.organization, + realms__is_active=True, + ) + + return get_user_model().objects.none() + class AssessmentStatusHistory(TimeStampedModel): assessment = models.ForeignKey( @@ -513,6 +526,9 @@ def save(self, *args, **kwargs): super().save(*args, **kwargs) self.assessment.update_rating() + def get_related_third_party_users(self): + return self.assessment.get_related_third_party_users() + class AnswerEvidence(TimeStampedModel): answer = models.ForeignKey( diff --git a/src/etools/applications/tpm/models.py b/src/etools/applications/tpm/models.py index cfd7ca624d..98872bbc0f 100644 --- a/src/etools/applications/tpm/models.py +++ b/src/etools/applications/tpm/models.py @@ -1,6 +1,7 @@ import itertools from django.conf import settings +from django.contrib.auth import get_user_model from django.db import connection, models from django.utils import timezone from django.utils.encoding import force_str @@ -355,6 +356,15 @@ def approve(self, mark_as_programmatic_visit=None, approval_comment=None, notify def get_object_url(self, **kwargs): return build_frontend_url('tpm', 'visits', self.id, 'details', **kwargs) + def get_related_third_party_users(self): + return get_user_model().objects.filter( + models.Q(pk__in=self.tpm_partner.staff_members.values_list('id')) | + models.Q(pk__in=self.tpm_partner_focal_points.values_list('id')) + ).filter( + realms__organization=self.tpm_partner.organization, + realms__is_active=True, + ) + class TPMVisitReportRejectComment(models.Model): rejected_at = models.DateTimeField(auto_now_add=True, verbose_name=_('Rejected At')) @@ -457,6 +467,9 @@ def get_mail_context(self, user=None, include_visit=True): return context + def get_related_third_party_users(self): + return self.tpm_visit.get_related_third_party_users() + class TPMActionPointManager(models.Manager): def get_queryset(self): diff --git a/src/etools/applications/tpm/tpmpartners/models.py b/src/etools/applications/tpm/tpmpartners/models.py index 614194ec78..f2c47b8064 100644 --- a/src/etools/applications/tpm/tpmpartners/models.py +++ b/src/etools/applications/tpm/tpmpartners/models.py @@ -52,6 +52,12 @@ def all_staff_members(self) -> models.QuerySet: ).values_list('user_id', flat=True) ) + def get_related_third_party_users(self): + return self.staff_members.filter( + realms__organization=self.organization, + realms__is_active=True, + ) + class TPMPartnerStaffMember(BaseStaffMember): """ diff --git a/src/etools/applications/users/tests/factories.py b/src/etools/applications/users/tests/factories.py index 212e7eb9c0..a8c149cdef 100644 --- a/src/etools/applications/users/tests/factories.py +++ b/src/etools/applications/users/tests/factories.py @@ -1,9 +1,11 @@ from django.conf import settings from django.contrib.auth import get_user_model from django.contrib.auth.models import Group +from django.db import connection from django.db.models import signals import factory +from django_tenants.utils import get_public_schema_name from factory.fuzzy import FuzzyText from etools.applications.action_points.models import PME @@ -35,6 +37,29 @@ class Meta: local_currency = factory.SubFactory(PublicsCurrencyFactory) +class DummyCountryFactory(CountryFactory): + class Meta: + model = models.Country + django_get_or_create = ('schema_name',) + + name = "Dummy Country" + schema_name = "dummy" + + @classmethod + def _create(cls, model_class, *args, **kwargs): + tenant = connection.tenant + try: + if tenant.schema_name != get_public_schema_name(): + tenant.deactivate() + cls._meta.model.auto_create_schema = False + country = super()._create(model_class, *args, **kwargs) + finally: + cls._meta.model.auto_create_schema = True + if tenant.schema_name != get_public_schema_name(): + tenant.activate() + return country + + @factory.django.mute_signals(signals.pre_save, signals.post_save) class ProfileFactory(factory.django.DjangoModelFactory): class Meta: diff --git a/src/etools/config/settings/base.py b/src/etools/config/settings/base.py index e76e29b3e5..05ca712176 100644 --- a/src/etools/config/settings/base.py +++ b/src/etools/config/settings/base.py @@ -596,7 +596,7 @@ def before_send(event, hint): ATTACHMENT_FILEPATH_PREFIX_FUNC = "etools.applications.attachments.utils.get_filepath_prefix" ATTACHMENT_FLAT_MODEL = "etools.applications.attachments.models.AttachmentFlat" ATTACHMENT_DENORMALIZE_FUNC = "etools.applications.attachments.utils.denormalize_attachment" -ATTACHMENT_PERMISSIONS = "etools.applications.attachments.permissions.IsInSchema" +ATTACHMENT_PERMISSIONS = "etools.applications.attachments.permissions.UNICEFAttachmentsPermission" ATTACHMENT_INVALID_FILE_TYPES = [ "application/json", "application/x-msdownload",